#9: Optional IQueryable<T> capability + in-house query AST
Closes #9 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
08cd32446f
commit
55516d4cf1
4 changed files with 547 additions and 0 deletions
|
|
@ -16,6 +16,7 @@ hardened auth / security stack. Header-only, oatpp 1.3+, C++17.
|
||||||
| `repo/Repository.hpp` + `IHistoryRepository.hpp` + `ITemporalEntity.hpp` + `TemporalAt.hpp` + `ActorContext.hpp` | Pure-abstract `Repository<TDto>` interface set distilled from fewo-webapp's per-entity `*Db` clients. Mixed UUID allocation on `save`, separate `IHistoryRepository<T>` for temporal versions, `ActorContext` placeholder for the upcoming scope-guard decorator. |
|
| `repo/Repository.hpp` + `IHistoryRepository.hpp` + `ITemporalEntity.hpp` + `TemporalAt.hpp` + `ActorContext.hpp` | Pure-abstract `Repository<TDto>` interface set distilled from fewo-webapp's per-entity `*Db` clients. Mixed UUID allocation on `save`, separate `IHistoryRepository<T>` for temporal versions, `ActorContext` placeholder for the upcoming scope-guard decorator. |
|
||||||
| `repo/TemporalRepository.hpp` | Decorator that wraps any `Repository<TDto : ITemporalEntity>` and turns it into a temporally-versioned one. `save` closes the prior live version and inserts a new one; `findByEntityIdAt(id, at)` returns the version live at a point in time; implements `IHistoryRepository<T>`. Inner adapter is expected to expose all rows (live + historical) and treat `save` as upsert keyed by `(entity_id, valid_from)`. |
|
| `repo/TemporalRepository.hpp` | Decorator that wraps any `Repository<TDto : ITemporalEntity>` and turns it into a temporally-versioned one. `save` closes the prior live version and inserts a new one; `findByEntityIdAt(id, at)` returns the version live at a point in time; implements `IHistoryRepository<T>`. Inner adapter is expected to expose all rows (live + historical) and treat `save` as upsert keyed by `(entity_id, valid_from)`. |
|
||||||
| `repo/ScopeGuardRepository.hpp` | Generic resource-scope decorator. Takes a `bool(ActorContext, TDto)` predicate at construction; gates every method on it. Throws `ScopeDeniedException` on deny (catchers translate to 403). Knows nothing about consumer-specific concepts like "property" or "tenant" — the predicate decides. |
|
| `repo/ScopeGuardRepository.hpp` | Generic resource-scope decorator. Takes a `bool(ActorContext, TDto)` predicate at construction; gates every method on it. Throws `ScopeDeniedException` on deny (catchers translate to 403). Knows nothing about consumer-specific concepts like "property" or "tenant" — the predicate decides. |
|
||||||
|
| `repo/IQueryable.hpp` | Optional capability for repos that resolve a typed query AST. `field<&Dto::col>().eq(...)` style DSL composes via `&&` / `||` / `!`; `Query<TDto>::toSql()` emits parameterised SQL plus a bind bag. Bounded surface — equality, range, IN, LIKE, NULL, ORDER BY, LIMIT/OFFSET. No joins, subqueries, or aggregates. Concrete repos opt in by deriving `IQueryable<TDto>`. |
|
||||||
|
|
||||||
## Consume via CMake
|
## Consume via CMake
|
||||||
|
|
||||||
|
|
|
||||||
323
include/oatpp-authkit/repo/IQueryable.hpp
Normal file
323
include/oatpp-authkit/repo/IQueryable.hpp
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_IQUERYABLE_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_IQUERYABLE_HPP
|
||||||
|
|
||||||
|
// Optional IQueryable<T> capability for the Repository<T> layer (authkit#9).
|
||||||
|
//
|
||||||
|
// A typed query DSL that emits parameterised SQL plus a bind bag. Bounded to
|
||||||
|
// equality / range / IN / LIKE / NULL / AND / OR / NOT / ORDER BY /
|
||||||
|
// LIMIT / OFFSET — no joins, subqueries, or aggregates. Concrete repos opt
|
||||||
|
// into the capability by deriving from `IQueryable<TDto>` and translating
|
||||||
|
// `Query<T>::toSql()` into their underlying store's prepared statements.
|
||||||
|
|
||||||
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
// ─── Schema registration ────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Concrete DTOs register their column / table names by specialising these
|
||||||
|
// two function templates. The primary templates are intentionally declared
|
||||||
|
// without a definition: forgetting to register a field is a hard compile or
|
||||||
|
// link error rather than a runtime surprise.
|
||||||
|
|
||||||
|
template <auto MemPtr>
|
||||||
|
const char* columnName();
|
||||||
|
|
||||||
|
template <typename TDto>
|
||||||
|
const char* tableName();
|
||||||
|
|
||||||
|
#define OATPP_AUTHKIT_REGISTER_FIELD(Dto, mem, colName) \
|
||||||
|
template <> \
|
||||||
|
inline const char* \
|
||||||
|
::oatpp_authkit::repo::columnName<&Dto::mem>() { return colName; }
|
||||||
|
|
||||||
|
#define OATPP_AUTHKIT_REGISTER_TABLE(Dto, name) \
|
||||||
|
template <> \
|
||||||
|
inline const char* \
|
||||||
|
::oatpp_authkit::repo::tableName<Dto>() { return name; }
|
||||||
|
|
||||||
|
// ─── Bind values ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using BindValue = std::variant<std::monostate, // null
|
||||||
|
std::int64_t,
|
||||||
|
double,
|
||||||
|
std::string,
|
||||||
|
bool>;
|
||||||
|
|
||||||
|
inline BindValue toBindValue(std::nullptr_t) { return BindValue{}; }
|
||||||
|
inline BindValue toBindValue(bool v) { return BindValue{v}; }
|
||||||
|
inline BindValue toBindValue(int v) { return BindValue{static_cast<std::int64_t>(v)}; }
|
||||||
|
inline BindValue toBindValue(long v) { return BindValue{static_cast<std::int64_t>(v)}; }
|
||||||
|
inline BindValue toBindValue(long long v) { return BindValue{static_cast<std::int64_t>(v)}; }
|
||||||
|
inline BindValue toBindValue(unsigned v) { return BindValue{static_cast<std::int64_t>(v)}; }
|
||||||
|
inline BindValue toBindValue(double v) { return BindValue{v}; }
|
||||||
|
inline BindValue toBindValue(float v) { return BindValue{static_cast<double>(v)}; }
|
||||||
|
inline BindValue toBindValue(const char* v) { return BindValue{std::string(v)}; }
|
||||||
|
inline BindValue toBindValue(const std::string& v) { return BindValue{v}; }
|
||||||
|
inline BindValue toBindValue(const oatpp::String& v) {
|
||||||
|
return v ? BindValue{std::string(*v)} : BindValue{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── AST nodes ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class AstNode {
|
||||||
|
public:
|
||||||
|
virtual ~AstNode() = default;
|
||||||
|
virtual void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>& binds) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CompareNode : public AstNode {
|
||||||
|
std::string col_;
|
||||||
|
const char* op_;
|
||||||
|
BindValue val_;
|
||||||
|
public:
|
||||||
|
CompareNode(std::string c, const char* o, BindValue v)
|
||||||
|
: col_(std::move(c)), op_(o), val_(std::move(v)) {}
|
||||||
|
void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>& binds) const override {
|
||||||
|
sql << col_ << ' ' << op_ << " ?";
|
||||||
|
binds.push_back(val_);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class InNode : public AstNode {
|
||||||
|
std::string col_;
|
||||||
|
std::vector<BindValue> vals_;
|
||||||
|
public:
|
||||||
|
InNode(std::string c, std::vector<BindValue> vs)
|
||||||
|
: col_(std::move(c)), vals_(std::move(vs)) {}
|
||||||
|
void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>& binds) const override {
|
||||||
|
if (vals_.empty()) { sql << "0"; return; } // empty IN ⇒ always false
|
||||||
|
sql << col_ << " IN (";
|
||||||
|
for (std::size_t i = 0; i < vals_.size(); ++i) {
|
||||||
|
if (i) sql << ", ";
|
||||||
|
sql << "?";
|
||||||
|
binds.push_back(vals_[i]);
|
||||||
|
}
|
||||||
|
sql << ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class IsNullNode : public AstNode {
|
||||||
|
std::string col_;
|
||||||
|
bool isNull_;
|
||||||
|
public:
|
||||||
|
IsNullNode(std::string c, bool n) : col_(std::move(c)), isNull_(n) {}
|
||||||
|
void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>&) const override {
|
||||||
|
sql << col_ << (isNull_ ? " IS NULL" : " IS NOT NULL");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CombineNode : public AstNode {
|
||||||
|
const char* sep_;
|
||||||
|
std::vector<std::shared_ptr<AstNode>> children_;
|
||||||
|
public:
|
||||||
|
CombineNode(const char* sep,
|
||||||
|
std::vector<std::shared_ptr<AstNode>> kids)
|
||||||
|
: sep_(sep), children_(std::move(kids)) {}
|
||||||
|
void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>& binds) const override {
|
||||||
|
if (children_.empty()) { sql << "1"; return; }
|
||||||
|
sql << "(";
|
||||||
|
for (std::size_t i = 0; i < children_.size(); ++i) {
|
||||||
|
if (i) sql << ' ' << sep_ << ' ';
|
||||||
|
children_[i]->emit(sql, binds);
|
||||||
|
}
|
||||||
|
sql << ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class NotNode : public AstNode {
|
||||||
|
std::shared_ptr<AstNode> child_;
|
||||||
|
public:
|
||||||
|
explicit NotNode(std::shared_ptr<AstNode> c) : child_(std::move(c)) {}
|
||||||
|
void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>& binds) const override {
|
||||||
|
sql << "NOT (";
|
||||||
|
child_->emit(sql, binds);
|
||||||
|
sql << ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Predicate composition wrapper ──────────────────────────────────────────
|
||||||
|
|
||||||
|
class Predicate {
|
||||||
|
std::shared_ptr<AstNode> node_;
|
||||||
|
public:
|
||||||
|
Predicate() = default;
|
||||||
|
explicit Predicate(std::shared_ptr<AstNode> n) : node_(std::move(n)) {}
|
||||||
|
|
||||||
|
bool empty() const noexcept { return !node_; }
|
||||||
|
std::shared_ptr<AstNode> node() const noexcept { return node_; }
|
||||||
|
|
||||||
|
void emit(std::ostringstream& sql,
|
||||||
|
std::vector<BindValue>& binds) const {
|
||||||
|
if (node_) node_->emit(sql, binds);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend Predicate operator&&(const Predicate& a, const Predicate& b) {
|
||||||
|
if (a.empty()) return b;
|
||||||
|
if (b.empty()) return a;
|
||||||
|
return Predicate{std::make_shared<CombineNode>(
|
||||||
|
"AND", std::vector<std::shared_ptr<AstNode>>{a.node_, b.node_})};
|
||||||
|
}
|
||||||
|
friend Predicate operator||(const Predicate& a, const Predicate& b) {
|
||||||
|
if (a.empty()) return b;
|
||||||
|
if (b.empty()) return a;
|
||||||
|
return Predicate{std::make_shared<CombineNode>(
|
||||||
|
"OR", std::vector<std::shared_ptr<AstNode>>{a.node_, b.node_})};
|
||||||
|
}
|
||||||
|
friend Predicate operator!(const Predicate& a) {
|
||||||
|
if (a.empty()) return a;
|
||||||
|
return Predicate{std::make_shared<NotNode>(a.node_)};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Field references ───────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// `field<&PersonDto::email>().eq("foo@bar")` resolves the column name through
|
||||||
|
// the `columnName<>` specialisation. Comparison methods return `Predicate`s
|
||||||
|
// that compose with `operator&&` / `||` / `!`.
|
||||||
|
|
||||||
|
template <auto MemPtr>
|
||||||
|
class Field {
|
||||||
|
public:
|
||||||
|
const char* column() const { return columnName<MemPtr>(); }
|
||||||
|
|
||||||
|
template <typename V> Predicate eq(V&& v) const { return mk("=", std::forward<V>(v)); }
|
||||||
|
template <typename V> Predicate ne(V&& v) const { return mk("!=", std::forward<V>(v)); }
|
||||||
|
template <typename V> Predicate lt(V&& v) const { return mk("<", std::forward<V>(v)); }
|
||||||
|
template <typename V> Predicate gt(V&& v) const { return mk(">", std::forward<V>(v)); }
|
||||||
|
template <typename V> Predicate le(V&& v) const { return mk("<=", std::forward<V>(v)); }
|
||||||
|
template <typename V> Predicate ge(V&& v) const { return mk(">=", std::forward<V>(v)); }
|
||||||
|
|
||||||
|
template <typename C>
|
||||||
|
Predicate in(const C& values) const {
|
||||||
|
std::vector<BindValue> bs;
|
||||||
|
for (auto& v : values) bs.push_back(toBindValue(v));
|
||||||
|
return Predicate{std::make_shared<InNode>(column(), std::move(bs))};
|
||||||
|
}
|
||||||
|
template <typename V>
|
||||||
|
Predicate in(std::initializer_list<V> values) const {
|
||||||
|
std::vector<BindValue> bs;
|
||||||
|
for (auto& v : values) bs.push_back(toBindValue(v));
|
||||||
|
return Predicate{std::make_shared<InNode>(column(), std::move(bs))};
|
||||||
|
}
|
||||||
|
|
||||||
|
Predicate like(const std::string& pat) const {
|
||||||
|
return Predicate{std::make_shared<CompareNode>(
|
||||||
|
column(), "LIKE", BindValue{pat})};
|
||||||
|
}
|
||||||
|
Predicate isNull() const { return Predicate{std::make_shared<IsNullNode>(column(), true)}; }
|
||||||
|
Predicate isNotNull() const { return Predicate{std::make_shared<IsNullNode>(column(), false)}; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename V>
|
||||||
|
Predicate mk(const char* op, V&& v) const {
|
||||||
|
return Predicate{std::make_shared<CompareNode>(
|
||||||
|
column(), op, toBindValue(std::forward<V>(v)))};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <auto MemPtr>
|
||||||
|
inline Field<MemPtr> field() { return Field<MemPtr>{}; }
|
||||||
|
|
||||||
|
// ─── Query builder ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct OrderBySpec {
|
||||||
|
std::string column;
|
||||||
|
bool ascending;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename TDto>
|
||||||
|
class Query {
|
||||||
|
Predicate where_;
|
||||||
|
std::vector<OrderBySpec> orderBy_;
|
||||||
|
std::int64_t limit_ = -1;
|
||||||
|
std::int64_t offset_ = 0;
|
||||||
|
public:
|
||||||
|
Query& where(Predicate p) {
|
||||||
|
where_ = where_.empty() ? std::move(p) : (where_ && std::move(p));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
template <auto MemPtr>
|
||||||
|
Query& orderBy(Field<MemPtr> f, bool ascending = true) {
|
||||||
|
orderBy_.push_back({f.column(), ascending});
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
template <auto MemPtr>
|
||||||
|
Query& orderByDesc(Field<MemPtr> f) { return orderBy(f, false); }
|
||||||
|
|
||||||
|
Query& limit(std::int64_t n) { limit_ = n; return *this; }
|
||||||
|
Query& offset(std::int64_t n) { offset_ = n; return *this; }
|
||||||
|
|
||||||
|
const Predicate& wherePredicate() const { return where_; }
|
||||||
|
const std::vector<OrderBySpec>& orderBySpecs() const { return orderBy_; }
|
||||||
|
std::int64_t limitValue() const { return limit_; }
|
||||||
|
std::int64_t offsetValue() const { return offset_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the query as a parameterised `SELECT * FROM <table> ...`.
|
||||||
|
* Concrete repositories take the returned text + bind bag and feed
|
||||||
|
* them into their underlying prepared-statement mechanism.
|
||||||
|
*/
|
||||||
|
struct Sql {
|
||||||
|
std::string text;
|
||||||
|
std::vector<BindValue> binds;
|
||||||
|
};
|
||||||
|
Sql toSql() const {
|
||||||
|
std::ostringstream s;
|
||||||
|
std::vector<BindValue> binds;
|
||||||
|
s << "SELECT * FROM " << tableName<TDto>();
|
||||||
|
if (!where_.empty()) {
|
||||||
|
s << " WHERE ";
|
||||||
|
where_.emit(s, binds);
|
||||||
|
}
|
||||||
|
if (!orderBy_.empty()) {
|
||||||
|
s << " ORDER BY ";
|
||||||
|
for (std::size_t i = 0; i < orderBy_.size(); ++i) {
|
||||||
|
if (i) s << ", ";
|
||||||
|
s << orderBy_[i].column
|
||||||
|
<< (orderBy_[i].ascending ? " ASC" : " DESC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (limit_ >= 0) s << " LIMIT " << limit_;
|
||||||
|
if (offset_ > 0) s << " OFFSET " << offset_;
|
||||||
|
return {s.str(), std::move(binds)};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Capability interface ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Optional capability for repositories that can resolve a typed `Query`.
|
||||||
|
*
|
||||||
|
* Concrete repos derive from `IQueryable<TDto>` (instead of plain
|
||||||
|
* `Repository<TDto>`) when they want to expose AST-driven filtering.
|
||||||
|
* Decorators stay agnostic — they wrap `Repository<TDto>` and downcast only
|
||||||
|
* when a caller specifically asks for the queryable surface.
|
||||||
|
*/
|
||||||
|
template <typename TDto>
|
||||||
|
class IQueryable : public Repository<TDto> {
|
||||||
|
public:
|
||||||
|
virtual oatpp::Vector<oatpp::Object<TDto>>
|
||||||
|
query(const Query<TDto>& q) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -29,3 +29,7 @@ add_test(NAME repository_interface COMMAND test_repository_interface)
|
||||||
add_executable(test_repository_decorators test_repository_decorators.cpp)
|
add_executable(test_repository_decorators test_repository_decorators.cpp)
|
||||||
target_link_libraries(test_repository_decorators PRIVATE oatpp::authkit oatpp::oatpp)
|
target_link_libraries(test_repository_decorators PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
add_test(NAME repository_decorators COMMAND test_repository_decorators)
|
add_test(NAME repository_decorators COMMAND test_repository_decorators)
|
||||||
|
|
||||||
|
add_executable(test_queryable test_queryable.cpp)
|
||||||
|
target_link_libraries(test_queryable PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
|
add_test(NAME queryable COMMAND test_queryable)
|
||||||
|
|
|
||||||
219
test/test_queryable.cpp
Normal file
219
test/test_queryable.cpp
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
// Tests for the oatpp-authkit#9 IQueryable<T> capability.
|
||||||
|
//
|
||||||
|
// Verifies that the AST emits the expected parameterised SQL and that the
|
||||||
|
// bind bag captures the values in order. No real database is involved —
|
||||||
|
// these tests exercise the SQL emitter and the builder API.
|
||||||
|
|
||||||
|
#include "oatpp-authkit/repo/IQueryable.hpp"
|
||||||
|
|
||||||
|
#include "oatpp/core/macro/codegen.hpp"
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MockQueryDto : public oatpp::DTO {
|
||||||
|
DTO_INIT(MockQueryDto, DTO)
|
||||||
|
DTO_FIELD(String, entity_id);
|
||||||
|
DTO_FIELD(String, name);
|
||||||
|
DTO_FIELD(String, email);
|
||||||
|
DTO_FIELD(Int64, age);
|
||||||
|
DTO_FIELD(Boolean, active);
|
||||||
|
};
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
OATPP_AUTHKIT_REGISTER_TABLE(MockQueryDto, "mock_query")
|
||||||
|
OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, entity_id, "entity_id")
|
||||||
|
OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, name, "name")
|
||||||
|
OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, email, "email")
|
||||||
|
OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, age, "age")
|
||||||
|
OATPP_AUTHKIT_REGISTER_FIELD(MockQueryDto, active, "active")
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int g_failures = 0;
|
||||||
|
|
||||||
|
#define REQUIRE(expr) do { \
|
||||||
|
if (!(expr)) { \
|
||||||
|
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \
|
||||||
|
++g_failures; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define REQUIRE_EQ(a, b) do { \
|
||||||
|
auto _av = (a); auto _bv = (b); \
|
||||||
|
if (!(_av == _bv)) { \
|
||||||
|
std::fprintf(stderr, "FAIL %s:%d %s == %s ('%s' vs '%s')\n", \
|
||||||
|
__FILE__, __LINE__, #a, #b, \
|
||||||
|
std::string(_av).c_str(), std::string(_bv).c_str()); \
|
||||||
|
++g_failures; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
using namespace oatpp_authkit::repo;
|
||||||
|
|
||||||
|
// Helper: pull a typed value out of a BindValue, or "<null>" / "<wrong>".
|
||||||
|
template <typename T>
|
||||||
|
std::string bindAsString(const BindValue& v) {
|
||||||
|
if (auto* p = std::get_if<T>(&v)) {
|
||||||
|
if constexpr (std::is_same_v<T, std::string>) return *p;
|
||||||
|
else return std::to_string(*p);
|
||||||
|
}
|
||||||
|
if (std::holds_alternative<std::monostate>(v)) return "<null>";
|
||||||
|
return "<wrong-type>";
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_equality_emits_parameterised_sql() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().eq("foo@bar"))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text,
|
||||||
|
std::string("SELECT * FROM mock_query WHERE email = ?"));
|
||||||
|
REQUIRE(sql.binds.size() == 1);
|
||||||
|
REQUIRE_EQ(bindAsString<std::string>(sql.binds[0]),
|
||||||
|
std::string("foo@bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_and_or_combines_predicates() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::active>().eq(true)
|
||||||
|
&& (field<&MockQueryDto::age>().gt(18)
|
||||||
|
|| field<&MockQueryDto::age>().lt(5)))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE "
|
||||||
|
"(active = ? AND (age > ? OR age < ?))"));
|
||||||
|
REQUIRE(sql.binds.size() == 3);
|
||||||
|
REQUIRE(std::get<bool>(sql.binds[0]) == true);
|
||||||
|
REQUIRE(std::get<std::int64_t>(sql.binds[1]) == 18);
|
||||||
|
REQUIRE(std::get<std::int64_t>(sql.binds[2]) == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_repeated_where_implies_and() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().eq("foo@bar"))
|
||||||
|
.where(field<&MockQueryDto::active>().eq(true))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE "
|
||||||
|
"(email = ? AND active = ?)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_range_emits_inclusive_and_exclusive() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::age>().ge(18)
|
||||||
|
&& field<&MockQueryDto::age>().le(65))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE (age >= ? AND age <= ?)"));
|
||||||
|
REQUIRE(std::get<std::int64_t>(sql.binds[0]) == 18);
|
||||||
|
REQUIRE(std::get<std::int64_t>(sql.binds[1]) == 65);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_in_with_multiple_values() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().in({"a@x", "b@x", "c@x"}))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE email IN (?, ?, ?)"));
|
||||||
|
REQUIRE(sql.binds.size() == 3);
|
||||||
|
REQUIRE_EQ(std::get<std::string>(sql.binds[0]), std::string("a@x"));
|
||||||
|
REQUIRE_EQ(std::get<std::string>(sql.binds[2]), std::string("c@x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_in_with_empty_list_is_always_false() {
|
||||||
|
std::vector<std::string> empty;
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().in(empty))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string("SELECT * FROM mock_query WHERE 0"));
|
||||||
|
REQUIRE(sql.binds.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_like_pattern_is_bound_not_interpolated() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::name>().like("Al%"))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE name LIKE ?"));
|
||||||
|
REQUIRE_EQ(std::get<std::string>(sql.binds[0]), std::string("Al%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_is_null_and_is_not_null() {
|
||||||
|
auto a = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().isNull())
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(a.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE email IS NULL"));
|
||||||
|
REQUIRE(a.binds.empty());
|
||||||
|
|
||||||
|
auto b = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().isNotNull())
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(b.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE email IS NOT NULL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_not_negates_predicate() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(!field<&MockQueryDto::active>().eq(true))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE NOT (active = ?)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_order_by_and_limit_offset() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::active>().eq(true))
|
||||||
|
.orderBy(field<&MockQueryDto::name>())
|
||||||
|
.orderByDesc(field<&MockQueryDto::age>())
|
||||||
|
.limit(50)
|
||||||
|
.offset(100)
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string(
|
||||||
|
"SELECT * FROM mock_query WHERE active = ? "
|
||||||
|
"ORDER BY name ASC, age DESC LIMIT 50 OFFSET 100"));
|
||||||
|
REQUIRE(std::get<bool>(sql.binds[0]) == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_no_where_no_clauses_is_plain_select() {
|
||||||
|
auto sql = Query<MockQueryDto>().toSql();
|
||||||
|
REQUIRE_EQ(sql.text, std::string("SELECT * FROM mock_query"));
|
||||||
|
REQUIRE(sql.binds.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_oatpp_string_value_is_unwrapped() {
|
||||||
|
auto sql = Query<MockQueryDto>()
|
||||||
|
.where(field<&MockQueryDto::email>().eq(oatpp::String("z@x")))
|
||||||
|
.toSql();
|
||||||
|
REQUIRE_EQ(std::get<std::string>(sql.binds[0]), std::string("z@x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_equality_emits_parameterised_sql();
|
||||||
|
test_and_or_combines_predicates();
|
||||||
|
test_repeated_where_implies_and();
|
||||||
|
test_range_emits_inclusive_and_exclusive();
|
||||||
|
test_in_with_multiple_values();
|
||||||
|
test_in_with_empty_list_is_always_false();
|
||||||
|
test_like_pattern_is_bound_not_interpolated();
|
||||||
|
test_is_null_and_is_not_null();
|
||||||
|
test_not_negates_predicate();
|
||||||
|
test_order_by_and_limit_offset();
|
||||||
|
test_no_where_no_clauses_is_plain_select();
|
||||||
|
test_oatpp_string_value_is_unwrapped();
|
||||||
|
|
||||||
|
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
||||||
|
return g_failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue