# Daf.SQLCute.Specification — Una especificación, una emisión
🌍 Idioma: English | Español
La mitad de SQL. Toma una especificación escrita con Daf.Specification y la baja a un IQuery de SQLCute: el filtro pasa a WHERE, el orden a ORDER BY y la página a LIMIT/OFFSET. Ese IQuery es lo único que produce.
uses Daf.SQLCute, Daf.SQLCute.Compiler, Daf.SQLCute.InMemory,
Daf.Specification, Daf.SQLCute.Specification;
var Q := TExprEmitter.Where(TQuery.New.From('Product'),
(Prop('Price') > 100) and (Prop('Active') = True));
// …una base de datos:
var Compiler: IQueryCompiler := TAnsiSqlCompiler.Create; // el compilador es contado: ha de tener dueño
var Sql := Compiler.Compile(Q);
// …o la lista que ya tienes cargada — el mismo objeto de consulta:
for var P in TInMemory.Elements<TProduct>(Q, Loaded) do ...Por qué hay una sola emisión
Los dos back-ends ya están aguas abajo del AST de SQLCute:
árbol de expresión
│ una emisión
▼
cláusulas SQLCute (IQuery)
│
┌──────┴──────┐
▼ ▼
TAnsiSqlCompiler TInMemory.Compile<T>
(+ dialectos) .Over(TEnumerable<T>)
│ │
▼ ▼
texto SQL ejecución en memoria
Así que “una consulta, dos back-ends” es una propiedad que este paquete hereda, no una que implemente. Aquí no hay traductor de expresión a Daf.Linq: al carril en memoria se llega por el mismo IQuery.
Conceptos
| Concepto | Descripción |
|---|---|
TExprEmitter.Where |
Expresión + IQuery → el mismo IQuery, con las condiciones |
TExprEmitter.Apply<T> |
Una especificación completa — filtro, orden, página — sobre una consulta |
TInMemoryRepository<T> |
El adaptador IRepository<T> en memoria, con caché de planes por regla |
ColumnFor |
Miembro → columna, mediante la estrategia aportada o tal cual |
WhereOpFor |
Los operadores del modelo mapeados al TWhereOp de SQLCute — una tabla |
EExprEmissionError |
Un nodo que no es un predicado, o un operador con la aridad equivocada |
La emisión conserva lo que escribiste
TWhereTree.Build reconstruye un árbol booleano desde una lista plana de cláusulas; no lo conserva. Lee esa lista como OR-de-tramos-AND, una gramática con exactamente un nivel de alternancia. Emite (a or b) and c como la lista plana [a AND, b OR, c AND] y el normalizador lo agrupa como a OR (b AND c) — un predicado distinto. Y como los dos back-ends consumen ese mismo árbol normalizado, los tests de paridad no pueden verlo: SQL y memoria coinciden, en la respuesta equivocada.
Por eso el emisor anida allí donde la alternancia se profundiza:
| Hijo | dentro de… | se emite como |
|---|---|---|
Or |
un tramo AND | un grupo anidado |
And |
una lista OR | en línea — el primer elemento lleva OR, el resto AND |
And |
un tramo AND | en línea (asociatividad) |
Or |
una lista OR | en línea (asociatividad) |
La negación se emite, nunca se reescribe:
| Escrito | Se emite como |
|---|---|
not (hoja) |
la negación propia de la hoja — IsNot, el operador emparejado (NOT LIKE, NOT IN, IS NOT NULL, NOT BETWEEN) o Negated |
not (subárbol) |
un grupo anidado negado — WhereNot(Callback) / OrWhereNot(Callback) |
No hay pasada de De Morgan. El SQL se lee como la forma que escribiste, no como un equivalente distribuido.
La garantía es semántica, no estructural: la lista de cláusulas normalizada y el árbol escrito evalúan igual bajo lógica de tres valores, para cualquier asignación de Cierto / Falso / Desconocido a las hojas.
La precedencia no se implementa aquí. a and b or c and d se emite como cuatro cláusulas planas sin ningún grupo propio del emisor, y el normalizador compartido lo agrupa como (a and b) or (c and d).
Amplitud sin nodos nuevos
Todo lo que SQLCute ya expresa se alcanza emitiendo la cláusula que lo dice:
Prop('Name').Like('%son%') // → cláusula LIKE
Prop('Name').StartsWith('And') // → WhereStarts
Prop('Price').Between(10, 200) // → cláusula BETWEEN
Prop('Price').IsIn([5, 120]) // → TWhereInClause
Prop('Price').IsNull // → IS NULLCada cláusula se produce a través de la superficie fluida de IQuery, que es también lo que mantiene TWhereInClause.Connector como la cadena 'AND'/'OR' con la que el normalizador lo compara, y lo que mantiene la emisión dentro de los cuatro tipos de cláusula que el normalizador acepta.
Pasar por esa superficie es también la razón de que el conjunto vacío no necesite ningún caso especial aquí:
Prop('Price').IsIn([]) // → WHERE (Price IS NULL AND Price IS NOT NULL)x IN (a, b) es x = a OR x = b, así que una lista vacía es la disyunción vacía — falsa sin consultar la columna, y verdadera para NotIn. El constructor de SQLCute resuelve eso al añadir la condición, de modo que esta capa emite IsIn([]) con la misma llamada que usa cualquier otro operador. Sin aridad mínima, sin error y sin rama propia.
El repositorio en memoria
var Repo: IRepository<TProduct> := TInMemoryRepository<TProduct>.Create(Loaded);
Repo.List(TSpecification<TProduct>.New
.Where(Prop('Active') = True)
.OrderByDesc('Price')
.Take(10));La fuente se referencia, nunca se posee ni se copia, y los planes se cachean por la clave de la regla — así que N reglas sobre una colección cargada compilan N planes y no recargan nada. Esto no es un doble de test: ejecutar varias reglas sobre un conjunto ya cargado es un camino de producción, con el mismo tipo de especificación que va a una base de datos.
Una consulta base acota el repositorio a un subconjunto, y se clona por regla para no mutar nunca la consulta del llamante:
TInMemoryRepository<TProduct>.Create(Loaded, TQuery.New.From('Product').Where('kind', 'tool'));La asimetría documentada
Traducible no es lo mismo que ejecutable en memoria. Las cláusulas que el carril en memoria no ha implementado — condiciones con subconsulta o EXISTS, GROUP BY/HAVING, joins, operaciones de conjuntos — compilan a SQL pero lanzan EInMemoryQueryError en el carril de lista única.
Ese fallo es ruidoso y en el momento de compilar el plan. El adaptador nunca lo captura y nunca filtra la colección en Delphi en su lugar — no hay evaluación de respaldo en el cliente.
Nótese que una especificación no puede llegar a ese camino por sí sola — el modelo de expresión no tiene EXISTS ni operador de SQL crudo, por diseño. Hace falta una consulta base para llegar ahí.
A medida que el back-end en memoria vaya cubriendo esas fases, este paquete las hereda sin cambio propio.
Documentación
| Recurso | Descripción |
|---|---|
| Guía de uso | La emisión, la costura de nombres, el adaptador, qué falla y cuándo |
| CatalogServiceSample | Un servicio de dominio sobre IRepository<T>, resuelto desde un TList<T> |
| Daf.Specification | El árbol, la superficie de autoría, especificaciones y el puerto |
| SQLCute In-Memory | El back-end al que esta emisión llega vía IQuery |
| SQLCute | El AST y la superficie fluida a la que esto emite |