# Daf.Specification — Typed Query Authoring, SQL-Free
🌍 Language: English | Español
The Specification pattern with a twist: the condition is written as Delphi code with operators and stays an inspectable tree instead of becoming a closure. The node set is deliberately narrower than a general expression tree — System.Linq.Expressions can carry anything, this cannot, so a rule that has no translation is unrepresentable rather than silently run client-side.
var Spec := TSpecification<TProduct>.New
.Where((Prop('Price') > 100) and (Prop('Active') = True))
.OrderByDesc('Price')
.Take(10);That value is a domain object. It can be asserted in a unit test, passed across a port, stored, logged — and later emitted to SQL by Daf.SQLCute.Specification. This package contains no SQL and does not reference SQLCute. A domain service that depends on it links none.
Concepts
| Concept | Description |
|---|---|
IExpr |
A node of the expression tree; ref-counted, no manual Free |
TExprKind |
The bounded node set: Compare / Logical / Unary + Member / Value / Param |
TCompareOp / TLogicalOp / TUnaryOp |
The model’s own operators — no raw SQL, no EXISTS |
TExpr |
The authoring record: overloaded operators, LIKE / BETWEEN / IN |
Prop(Name) |
A member reference by name — the always-available path |
TEntityExpr<T> |
RTTI proxy: an entity’s members as leaves, validated where written |
TPropExpr<T> |
The optional metamodel leaf, for compile-time-checked authoring |
INameResolver |
The one seam: member → column, type → table |
TSpecification<T> |
A reusable rule: filter + ordering + paging |
IRepository<T> |
The port; T is constrained to no base class |
ExprToText |
A canonical rendering — for assertions, cache keys and diagnostics |
Why it is a tree and not a closure
Commons already has a closure-based filter, TFilterSpec<T> in Daf.FilterSpec, and Daf.Linq runs it over loaded sequences. This package borrows that operator-overloading shape and none of its type, because a closure is opaque: you can run it, never translate it.
| World | Filter | Runs on |
|---|---|---|
| SQL, or one query for both back-ends | this package → IQuery |
a database or a loaded collection |
| Already-loaded objects, arbitrary Delphi code | Daf.Linq + TFilterSpec<T> |
a materialized sequence |
They are siblings serving different worlds. Which is why TSpecification<T> has no overload taking a closure: a specification that cannot be translated must be unrepresentable rather than silently evaluated client-side.
The model is data-driven
Six node types, three operator enums. Adding a comparison is a new enum value, never a new class — the same binary node carries every operator, and Like, Between and In are emitted as the SQLCute clause that already says them.
Prop('Name').Like('%son%') // one node, operator Like
Prop('Price').Between(10, 200) // one node, operator Between
Prop('Price').IsIn([5, 120]) // one node, operator In
Prop('Price').IsNull
not (Prop('Active') = True)Values are carried as a single Variant — the representation SQLCute clauses already hold — so no typed-accessor parameter model is reimplemented. NULL is Commons’ TNullable, narrowed at the authoring edge:
Prop('Id') = SomeGuid // typed overload → canonical string
Prop('Age') = TExprValue.From<Integer>(N) // TNullable → its value, or NullNarrowing happens where you write it, never inside the emission, so both lanes see the same canonical value.
Names
One strategy answers both naming questions:
INameResolver.ColumnFor(EntityType, Member) // default: the member name
INameResolver.TableFor(EntityType) // default: type name minus a leading TThe table default strips T only when an upper-case letter follows, so TProduct → Product while Thing is left alone. No pluralization and no case conversion: those are naming policy, and this layer has none.
Keys, lock columns, attributes and instance-driven DML (Update(Instance)) are absent.
Failing where the mistake is
A member name that resolves to nothing raises at the point the expression is authored, naming the member and the type. The hazard being avoided is a pass-through default: it makes a wrong name look right on whichever path tolerates it — SQL will happily send an unmapped column to the server, while the in-memory lane cannot — so the divergence surfaces far from its cause and on only one back-end.
Documentation
| Resource | Description |
|---|---|
| Usage Guide | Authoring, the proxy, the metamodel, specifications |
| Daf.SQLCute.Specification | The emission into IQuery, and the in-memory adapter |
| SQLCute | The query builder this eventually emits into |
| Daf.Linq | The closure world — the sibling, not the rival |