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

Closed
opened 2026-04-27 21:22:28 +02:00 by uwe.admin · 2 comments
Owner

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 webapp-scaffold
  • 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.
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 webapp-scaffold - 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.
uwe.admin added the
evaluated
label 2026-04-27 21:22:28 +02:00
Author
Owner

Agent Evaluation

Drafted as a follow-on capability after in-conversation discussion of predicate-based queries. Body above is the evaluation.

Feasibility: Bounded design — pointer-to-member + visitor pattern is well-trodden ground. The 500-LOC budget is the discipline that keeps it tractable.

Impact: Closes the last common reason for controllers to reach back into raw SQL: complex filters. Without it, the Repository abstraction leaks for any non-trivial query.

Effort: Medium. AST + visitor + tests. Larger than the decorators (uwe.admin/webapp-scaffold#9) but smaller than adopting sqlite_orm/sqlpp11 wholesale.

Recommendation: Accept, but do not implement until parent uwe.admin/fewo-webapp#458 Phases 1–3 land. Real query patterns from the pilot inform the AST surface — designing it sooner risks getting the operator set wrong.

## Agent Evaluation Drafted as a follow-on capability after in-conversation discussion of predicate-based queries. Body above is the evaluation. **Feasibility:** Bounded design — pointer-to-member + visitor pattern is well-trodden ground. The 500-LOC budget is the discipline that keeps it tractable. **Impact:** Closes the last common reason for controllers to reach back into raw SQL: complex filters. Without it, the Repository<T> abstraction leaks for any non-trivial query. **Effort:** Medium. AST + visitor + tests. Larger than the decorators (uwe.admin/webapp-scaffold#9) but smaller than adopting sqlite_orm/sqlpp11 wholesale. **Recommendation:** Accept, but **do not implement until parent uwe.admin/fewo-webapp#458 Phases 1–3 land**. Real query patterns from the pilot inform the AST surface — designing it sooner risks getting the operator set wrong.
Author
Owner

Closed in favour of uwe.admin/oatpp-authkit#9 per the Option A decision (this issue's #8 thread). C++ work belongs in oatpp-authkit, not in this TypeScript scaffold package.

Closed in favour of uwe.admin/oatpp-authkit#9 per the Option A decision (this issue's #8 thread). C++ work belongs in oatpp-authkit, not in this TypeScript scaffold package.
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/webapp-scaffold#10
No description provided.