Optional IQueryable<T> capability + in-house query AST (post-pilot follow-on) #9

Closed
opened 2026-04-27 21:30:16 +02:00 by uwe.admin · 3 comments
Owner

Migrated from uwe.admin/webapp-scaffold#10 per the Option A decision. Builds on uwe.admin/oatpp-authkit#7, uwe.admin/oatpp-authkit#8.

AST + IQueryable capability lands under oatpp-authkit/include/oatpp-authkit/repo/ next to the core repo headers.


Optional follow-on capability for the Repository<T> layer (parent: uwe.admin/fewo-webapp#458). Not blocking the current refactor — the pilot (uwe.admin/fewo-webapp#457) and Phases 4–7 should land first.

Why

Repository<T>::list(filter) covers simple cases. Anything beyond "single-field equality" currently forces controllers to either reach into the underlying *Db for a hand-written QUERY, or pull rows and filter in C++. Both undo the point of the abstraction.

A typed query builder restores the abstraction without the cost of a std::function predicate (which can't push down to SQL — see in-conversation discussion).

Scope

Add to webapp-scaffold a small expression-tree DSL and an optional capability interface:

// Capability — concrete repos opt in
template <typename TDto>
class IQueryable : public Repository<TDto> {
public:
    virtual oatpp::Vector<oatpp::Object<TDto>>
        query(const Query<TDto>& q) = 0;
};

// Usage at call site
auto results = repo.query(
    Query<PersonDto>()
        .where(field<&PersonDto::email>().eq("foo@bar"))
        .where(field<&PersonDto::active>().eq(true))
        .orderBy(field<&PersonDto::name>())
        .limit(50));

Internally:

  • A small AST (AndNode, OrNode, EqNode, LtNode, GtNode, InNode, LikeNode, IsNullNode, plus OrderBy, Limit, Offset).
  • Visitor that walks the AST and emits parameterized SQL + bind values.
  • Field references via pointer-to-member + a tiny Schema<TDto> registration that maps members to column names. (Kept generic so the DSL doesn't know about specific tables.)

Constraints

  • Strictly bounded surface — equality, range, IN, LIKE, NULL, AND/OR, NOT, ORDER BY, LIMIT/OFFSET. No joins, no subqueries, no aggregates in v1.
  • ≤500 LOC of header-only template code is the size budget. If it exceeds, the design is wrong — regroup, don't expand scope.
  • Generates parameterized SQL only (no string interpolation of user values).
  • Compatible with existing oatpp DbClient — emits SQL strings + a parameter bag the concrete repo can hand to oatpp's prepared-statement mechanism. Does not replace QUERY macros for simple cases.

Out of scope (explicit non-goals)

  • Joins / relationship navigation — repositories stay per-aggregate
  • Aggregations (COUNT, SUM, GROUP BY) — separate issue if ever needed
  • Update/delete via DSL — saves still go through Repository<T>::save
  • Replacing sqlite_orm / sqlpp11 — if the in-house version proves too constraining, that's the trigger to evaluate adopting an external library. Don't pre-empt.
  • Any controller migration to IQueryable — that comes per-entity, after the pilot validates the Repository layer

Sequencing

  1. Land Phases 1–3 of parent (#458) first
  2. Validate the Repository design holds up on Person, Property, Booking
  3. Then this issue: only at that point do we know enough about real query patterns to design the right AST surface

Acceptance

  • IQueryable<T> interface, Query<T> builder, AST nodes, and SQL emitter compile in oatpp-authkit
  • Unit tests cover: equality, AND/OR, range, IN, LIKE, NULL, ORDER BY, LIMIT — each verifies both the emitted SQL string and the parameter bag
  • One concrete repo (likely PersonRepository) demonstrates the end-to-end path against real SQLite, but that wiring lives in fewo-webapp as a follow-up issue
  • Total scaffold-side code ≤500 LOC

Decision deferred to implementation time

  • Whether Schema<TDto> registration is a class template specialization, a free function, or a macro. Pick whichever produces the cleanest call site once we have 2–3 entities to test against.
Migrated from uwe.admin/webapp-scaffold#10 per the Option A decision. Builds on uwe.admin/oatpp-authkit#7, uwe.admin/oatpp-authkit#8. AST + IQueryable<T> capability lands under `oatpp-authkit/include/oatpp-authkit/repo/` next to the core repo headers. --- Optional follow-on capability for the `Repository<T>` layer (parent: uwe.admin/fewo-webapp#458). **Not blocking** the current refactor — the pilot (uwe.admin/fewo-webapp#457) and Phases 4–7 should land first. ## Why `Repository<T>::list(filter)` covers simple cases. Anything beyond "single-field equality" currently forces controllers to either reach into the underlying `*Db` for a hand-written `QUERY`, or pull rows and filter in C++. Both undo the point of the abstraction. A typed query builder restores the abstraction without the cost of a `std::function` predicate (which can't push down to SQL — see in-conversation discussion). ## Scope Add to webapp-scaffold a small expression-tree DSL and an optional capability interface: ```cpp // Capability — concrete repos opt in template <typename TDto> class IQueryable : public Repository<TDto> { public: virtual oatpp::Vector<oatpp::Object<TDto>> query(const Query<TDto>& q) = 0; }; // Usage at call site auto results = repo.query( Query<PersonDto>() .where(field<&PersonDto::email>().eq("foo@bar")) .where(field<&PersonDto::active>().eq(true)) .orderBy(field<&PersonDto::name>()) .limit(50)); ``` Internally: - A small AST (`AndNode`, `OrNode`, `EqNode`, `LtNode`, `GtNode`, `InNode`, `LikeNode`, `IsNullNode`, plus `OrderBy`, `Limit`, `Offset`). - Visitor that walks the AST and emits parameterized SQL + bind values. - Field references via pointer-to-member + a tiny `Schema<TDto>` registration that maps members to column names. (Kept generic so the DSL doesn't know about specific tables.) ## Constraints - **Strictly bounded surface** — equality, range, IN, LIKE, NULL, AND/OR, NOT, ORDER BY, LIMIT/OFFSET. No joins, no subqueries, no aggregates in v1. - ≤500 LOC of header-only template code is the size budget. If it exceeds, the design is wrong — regroup, don't expand scope. - Generates parameterized SQL only (no string interpolation of user values). - Compatible with existing oatpp `DbClient` — emits SQL strings + a parameter bag the concrete repo can hand to oatpp's prepared-statement mechanism. Does not replace `QUERY` macros for simple cases. ## Out of scope (explicit non-goals) - Joins / relationship navigation — repositories stay per-aggregate - Aggregations (`COUNT`, `SUM`, `GROUP BY`) — separate issue if ever needed - Update/delete via DSL — saves still go through `Repository<T>::save` - Replacing sqlite_orm / sqlpp11 — if the in-house version proves too constraining, *that's* the trigger to evaluate adopting an external library. Don't pre-empt. - Any controller migration to `IQueryable` — that comes per-entity, after the pilot validates the Repository layer ## Sequencing 1. Land Phases 1–3 of parent (#458) first 2. Validate the Repository<T> design holds up on Person, Property, Booking 3. Then this issue: only at that point do we know enough about real query patterns to design the right AST surface ## Acceptance - `IQueryable<T>` interface, `Query<T>` builder, AST nodes, and SQL emitter compile in oatpp-authkit - Unit tests cover: equality, AND/OR, range, IN, LIKE, NULL, ORDER BY, LIMIT — each verifies both the emitted SQL string and the parameter bag - One concrete repo (likely PersonRepository) demonstrates the end-to-end path against real SQLite, but that wiring lives in fewo-webapp as a follow-up issue - Total scaffold-side code ≤500 LOC ## Decision deferred to implementation time - Whether `Schema<TDto>` registration is a class template specialization, a free function, or a macro. Pick whichever produces the cleanest call site once we have 2–3 entities to test against.
Author
Owner

Evaluation carried over from closed uwe.admin/webapp-scaffold#10; the migration comment at the top of this issue body summarises the prior eval. No additional design questions remain.

Evaluation carried over from closed uwe.admin/webapp-scaffold#10; the migration comment at the top of this issue body summarises the prior eval. No additional design questions remain.
uwe.admin added the
evaluated
label 2026-04-27 21:31:38 +02:00
u.schuster added the
accepted
label 2026-04-29 12:50:56 +02:00
Author
Owner

Implemented #9 → commit 55516d4 (include/oatpp-authkit/repo/IQueryable.hpp, ~250 LOC; test/test_queryable.cpp, 12 cases; README updated). Surface: equality / range / IN / LIKE / NULL / AND / OR / NOT / ORDER BY / LIMIT / OFFSET. field<&Dto::col>() with macro-registered column names; Query<T>::toSql() returns text + bind bag. All 7 ctest cases pass.

Implemented #9 → commit 55516d4 (`include/oatpp-authkit/repo/IQueryable.hpp`, ~250 LOC; `test/test_queryable.cpp`, 12 cases; README updated). Surface: equality / range / IN / LIKE / NULL / AND / OR / NOT / ORDER BY / LIMIT / OFFSET. `field<&Dto::col>()` with macro-registered column names; `Query<T>::toSql()` returns text + bind bag. All 7 ctest cases pass.
Author
Owner

Implemented in 55516d4 (and verified — 7/7 tests pass, IQueryable.hpp 323 LOC ≤500 budget). Auto-closed via Closes #9.

Implemented in 55516d4 (and verified — 7/7 tests pass, IQueryable.hpp 323 LOC ≤500 budget). Auto-closed via `Closes #9`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: uwe.admin/oatpp-authkit#9
No description provided.