#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/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/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
|
||||
|
||||
|
|
|
|||
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)
|
||||
target_link_libraries(test_repository_decorators PRIVATE oatpp::authkit oatpp::oatpp)
|
||||
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