SQLCute — Constructor Fluido de SQL para Delphi
🌍 Idioma: English | Español
Construye consultas SQL con una API fluida, segura en tipos. Sin concatenación de cadenas, sin SQL crudo — solo encadenamiento de métodos que compila a SQL parametrizado.
Conceptos
| Concepto | Descripción |
|---|---|
IQuery |
Interfaz fluida — cada método devuelve IQuery para encadenamiento |
TQuery.New |
Punto de entrada — crea una query con conteo de referencias, sin necesidad de Free |
TSQLResult |
Resultado: cadena SQL + array Bindings de parámetros posicionales |
IQueryCompiler |
Estrategia para generación de SQL específica del dialecto (ANSI por defecto, o un compilador de dialecto) |
Inicio Rápido
uses Daf.SQLCute, Daf.SQLCute.Compiler;
var Q := TQuery.New
.From('pedidos')
.Select(['id', 'total', 'estado'])
.Where('estado', 'pendiente')
.Where('total', '>', 100)
.OrderByDesc('creado_en')
.Limit(20);
var Compiler: IQueryCompiler := TAnsiSqlCompiler.Create; // el compilador es contado: ha de tener dueño
var R := Compiler.Compile(Q);
// R.SQL → 'SELECT id, total, estado FROM pedidos WHERE estado = ? AND total > ? ORDER BY creado_en DESC LIMIT 20'
// R.Bindings → ['pendiente', 100]Patrones Comunes
Filtrar y paginar
TQuery.New
.From('usuarios')
.Where('activo', True)
.ForPage(Pagina, 25) // LIMIT 25 OFFSET (Pagina-1)*25JOIN
TQuery.New
.From('pedidos')
.Join('clientes', 'pedidos.cliente_id', 'clientes.id')
.Select(['pedidos.id', 'clientes.nombre'])Agregación
TQuery.New
.From('ventas')
.GroupBy('producto_id')
.SelectCount('*', 'qty')
.SelectSum('importe', 'total')
.HavingRaw('count(*) > 10')DML
// INSERT
TQuery.New.From('logs')
.AsInsert(['nivel', 'mensaje'], ['error', 'disco lleno']);
// UPDATE
TQuery.New.From('usuarios')
.Where('id', 42)
.AsUpdate(['email'], ['nuevo@ejemplo.com']);
// DELETE
TQuery.New.From('sesiones')
.Where('expirado', True)
.AsDelete;Una consulta declara qué sentencia es: lo fijan las fachadas de DML, y lo lees con Query.Kind (Select por defecto). Los back-ends lo consultan, en vez de deducirlo de qué cláusulas hay. Declarar dos tipos distintos en la misma consulta se queda con el último y retira las cláusulas del tipo descartado — igual que una llamada posterior de una cadena fluida anula a la anterior:
TQuery.New.From('users').AsInsert(['name'], ['Ann']).AsDelete;
// Kind es Delete, la inserción ya no está — compila DELETE FROM usersCompilador de dialecto
uses Daf.SQLCute, Daf.SQLCute.Compiler.Postgres;
var Compiler: IQueryCompiler := TPostgresCompiler.Create; // contado por referencia: ha de tener dueño
var R := Compiler.Compile(Q);
// identificadores entrecomillados con "", marcadores ?Documentación
| Recurso | Descripción |
|---|---|
| Guía de Uso | Referencia completa de la API con todos los métodos |
| SELECT & FROM | Columnas, DISTINCT, agregados, ORDER BY, LIMIT |
| WHERE | Condiciones, IN, EXISTS, grupos, LIKE, operaciones de fecha |
| JOINs | Todos los tipos de join y callbacks |
| GROUP BY / HAVING | Agrupación y filtros de agregados |
| Operaciones de Conjunto | UNION, INTERSECT, EXCEPT, paginación |
| DML | INSERT, UPDATE, DELETE |
| Subconsultas y CTEs | Tablas derivadas, WITH, CTEs recursivos |
| Operaciones de Cadena | LIKE, starts/ends/contains |
| Operaciones de Fecha | WhereDate, casting por dialecto |
| Compiladores de Dialecto | Postgres, MySQL, SQLite, SQL Server, Oracle, Firebird |
| Back-End en Memoria | Ejecuta el mismo IQuery sobre una colección ya cargada |
Abstracciones
SQLCute.Abstractions
🌍 Idioma: English | Español
El AST de la consulta y los contratos que habla un back-end. Este módulo define solo interfaces, enumerados y nodos de cláusula — sin builder, sin compilador, sin motor. Referéncialo cuando escribas un compilador de dialecto o un back-end propio, o cuando una librería tuya pase consultas de un lado a otro sin enlazar ninguno de los dos.
Qué hay en este módulo
Dos units, y el orden importa — Daf.SQLCute usa Daf.SQLCute.Clauses:
| Unit | Contiene |
|---|---|
Daf.SQLCute.Clauses |
Los nodos de cláusula — TAbstractClause y 21 de sus 22 descendientes — y los enumerados que llevan |
Daf.SQLCute |
IQuery, IQueryCompiler, TSQLResult, TQueryKind, la factoría TQuery, dos helpers, y el vigesimosegundo nodo de cláusula — TFromSubqueryClause, que necesita IQuery |
Las dos units son {$SCOPEDENUMS ON}, así que cada valor de enumerado va cualificado: TWhereOp.Equal, TSortDir.Desc, TJoinType.Left.
Los dos contratos
| Tipo | Papel |
|---|---|
IQuery |
Builder fluido — cada método devuelve IQuery, con conteo de referencias, sin Free |
IQueryCompiler |
function Compile(const Query: IQuery): TSQLResult — un método, nada más |
TSQLResult es un record con SQL: string y Bindings: TArray<Variant>. Los marcadores ? de SQL corresponden por posición a los elementos de Bindings; un compilador de dialecto, en su estilo de marcador nativo, emite en su lugar $1 o :p1, en el mismo orden. TSQLResult.Empty es el valor cero.
Leer una consulta: el AST es público
IQuery expone dos miembros que un back-end lee:
| Miembro | Devuelve |
|---|---|
Clauses |
TArray<TAbstractClause> — los nodos de cláusula, en el orden en que se añadieron |
Kind |
TQueryKind — Select, Insert, Update o Delete |
Las fachadas de DML (AsInsert, AsInsertRows, AsInsertFrom, AsUpdate, AsDelete) escriben Kind, así que un back-end tiene el tipo de sentencia sin recorrer las cláusulas. Select es el valor por defecto.
La consulta es la dueña de sus nodos. Clauses entrega referencias: no las liberes, y no conserves una más allá del IQuery del que salió. IQuery.Clone copia en profundidad la consulta entera, y para eso sirve el método Clone de cada nodo.
Los nodos de cláusula
| Área | Nodos |
|---|---|
| SELECT | TSelectClause (un array de TSelectColumn), TDistinctClause (marcador, sin campos) |
| FROM | TFromClause, TFromRawClause, TFromSubqueryClause |
| JOIN | TJoinClause, TJoinSubqueryClause |
| WHERE | TWhereClause, TNestedWhereClause, TWhereInClause, TDateWhereClause |
| GROUP BY / HAVING | TGroupByClause, THavingClause |
| ORDER BY | TOrderByClause |
| Paginación | TLimitClause, TOffsetClause |
| Operaciones de conjunto | TUnionClause |
| CTE | TWithClause |
| SQL literal | TRawClause |
| DML | TInsertClause, TUpdateSetClause, TDeleteClause (marcador — la tabla sale de TFromClause) |
Una llamada produce un nodo: WhereIn añade un TWhereInClause, y AsUpdate añade un TUpdateSetClause por cada par columna–valor.
Tres campos deciden cómo se lee un nodo, y aparecen repartidos por el árbol:
IsRaw(TSelectColumn,TOrderByClause,TGroupByClause,TUpdateSetClause) — emite el valor literalmente.Connector: TBoolOp(TWhereClause,THavingClause,TNestedWhereClause,TDateWhereClause) — cómo enlaza esta condición con la anterior. ⚠️TWhereInClause.Connectores unstring,'AND'o'OR'.SubQuery— una consulta anidada, de tipoIInterface(ver más abajo).
Una condición LIKE es un TWhereClause cuyo Op es Like o NotLike. Con CaseSensitive = False su Value lleva el patrón ya en minúsculas, y EscapeChar vale #0 cuando la llamada no declaró carácter de escape.
TJoinClause lleva tres formas de expresar su condición ON, con una precedencia: manda ConditionClauses (venida de un callback), luego Col1/Op/Col2, y por último la cadena Condition.
Las subconsultas son de tipo IInterface
Daf.SQLCute.Clauses se compila antes que la unit que declara IQuery, así que un nodo que guarda una consulta anidada la declara como IInterface. Haz el cast al leerla:
if Assigned(W.SubQuery) then
begin
var Inner := W.SubQuery as IQuery;
// recorrer Inner.Clauses
end;Los nodos con campo de subconsulta: TWhereClause, TJoinSubqueryClause, TUnionClause, TWithClause, TInsertClause — y TSelectColumn, que es un record dentro de TSelectClause y no un nodo propio.
⚠️ TFromSubqueryClause es la excepción: se declara en Daf.SQLCute, junto a IQuery, así que su campo SubQuery ya es un IQuery y no necesita cast.
La jerarquía es cerrada
Los descendientes de TAbstractClause son los que declara este módulo. Un back-end atiende esos y ningún otro — no hay punto de registro para un nodo propio, y nada fuera de este módulo deriva de TAbstractClause.
Los enumerados
| Enumerado | Valores |
|---|---|
TWhereOp |
Equal, NotEqual, Less, LessOrEqual, Greater, GreaterOrEqual, Like, NotLike, &In, NotIn, IsNull, IsNotNull, &Between, NotBetween, &Exists, NotExists, Raw |
TBoolOp |
opAnd, opOr |
TSortDir |
Asc, Desc |
TNullsOrder |
Default, First, Last |
TJoinType |
Inner, Left, Right, Cross, FullOuter |
TUnionKind |
Union, UnionAll, Intersect, &Except, IntersectAll, ExceptAll |
TDatePart |
dpDate, dpTime, dpYear, dpMonth, dpDay, dpHour, dpMinute |
TQueryKind |
Select, Insert, Update, Delete |
⚠️ Cuatro valores chocan con palabras reservadas de Delphi y llevan el & en su declaración, así que también lo tecleas tú: TWhereOp.&In, TWhereOp.&Between, TWhereOp.&Exists, TUnionKind.&Except.
TNullsOrder.Default no emite nada y deja la colocación de los NULL a la base de datos, donde cada dialecto tiene su propia regla. En un TOrderByClause con IsRaw activo, Nulls se ignora — un término literal sale tal cual.
Los dos helpers
ParseWhereOp(const Op: string): TWhereOp traduce una cadena de operador, sin distinguir mayúsculas y quitando los espacios de los extremos. Acepta once escrituras:
'=' '<>' '!=' '<' '<=' '>' '>=' 'like' 'not like' 'in' 'not in'
Cualquier otra levanta EArgumentException:
Unknown SQL operator: "=="
Los demás valores de TWhereOp — IsNull, IsNotNull, &Between, NotBetween, &Exists, NotExists, Raw — no tienen escritura en cadena. Los métodos de IQuery que producen esas condiciones los ponen directamente.
QueryKindName(const K: TQueryKind): string da el nombre del valor ('Select', 'Insert', …), para que una guarda pueda nombrar el tipo que recibió.
Conseguir un IQuery: TQuery.New necesita una factoría
TQuery.New devuelve un builder nuevo, y este módulo no tiene implementación con la que construirlo. Daf.SQLCute.Core — en el paquete SQLCute — registra una en su sección initialization. Enlazar ese paquete es lo que hace funcionar a TQuery.New.
Enlaza solo Daf.SQLCute.Abstractions y la primera llamada levanta EInvalidOpException:
IQuery factory not registered. Ensure Daf.SQLCute.Core is linked.
Para aportar una implementación propia de IQuery, regístrala:
uses Daf.SQLCute;
TQuery.RegisterFactory(
function: IQuery
begin
Result := TMyQuery.Create;
end);TIQueryFactory es reference to function: IQuery. Manda el último registro. Un argumento nil levanta EInvalidOpException:
Argument Factory cannot be nil
⚠️ RegisterIQueryFactory es el mismo registro como función suelta y está deprecado. Reenvía a TQuery.RegisterFactory, así que los dos puntos de entrada se comportan igual; el código nuevo llama al método de clase.
Quién implementa estos contratos
| Paquete | Qué aporta |
|---|---|
SQLCute |
IQuery (Daf.SQLCute.Core) e IQueryCompiler (TAnsiSqlCompiler), más seis compiladores de dialecto que derivan de él: Firebird, MySQL, Oracle, PostgreSQL, SQL Server, SQLite |
SQLCute.InMemory |
Un back-end que no emite SQL: consume los mismos Clauses y Kind y los ejecuta sobre colecciones Delphi ya cargadas |
SQLCute.Specification |
Produce un IQuery a partir del filtro, la ordenación y la página de una Daf.Specification |
Una consulta completa, de punta a punta, está en SQLCute/samples/SQLCuteSample.
Dependencias
Sin dependencias de otros módulos DAF. Solo la RTL de Delphi.
El builder y los compiladores están en SQLCute.