# SQLCute — Guía de Uso
🌍 Idioma: English | Español
Introducción
SQLCute construye SQL parametrizado mediante una cadena fluida de métodos. El punto de entrada siempre es TQuery.New:
uses Daf.SQLCute, Daf.SQLCute.Compiler;
var Q := TQuery.New // IQuery — con conteo de referencias, sin Free
.From('productos')
.Where('estado', 'activo')
.OrderBy('nombre')
.Limit(50);
var Compiler: IQueryCompiler := TAnsiSqlCompiler.Create; // el compilador es contado: ha de tener dueño
var R := Compiler.Compile(Q);
// R.SQL → 'SELECT * FROM productos WHERE estado = ? ORDER BY nombre LIMIT 50'
// R.Bindings → ['activo']TSQLResult siempre contiene:
| Campo | Tipo | Descripción |
|---|---|---|
SQL |
string |
El SQL compilado con marcadores ? (en el estilo nativo, los compiladores de dialecto escriben $1, :p1, @p0) |
Bindings |
TArray<Variant> |
Valores posicionales que corresponden a los marcadores |
Pasa R.SQL y R.Bindings a tu driver de base de datos — SQLCute nunca ejecuta consultas por sí solo.
Tabla de Contenidos
- SELECT & FROM — columnas, DISTINCT, agregados, ORDER BY, LIMIT/OFFSET
- WHERE — igualdad, comparaciones, NULL, BETWEEN, IN, EXISTS, grupos, columnas
- JOINs — INNER, LEFT, RIGHT, CROSS, FULL OUTER, 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 de fecha por dialecto
- Compiladores de Dialecto — entrecomillado, marcadores, matriz de dialectos
El Patrón del Compilador
Todo IQuery es agnóstico al compilador. Eliges el dialecto en el momento de compilar:
uses Daf.SQLCute; // IQueryCompiler
uses Daf.SQLCute.Compiler; // TAnsiSqlCompiler (por defecto)
uses Daf.SQLCute.Compiler.Postgres; // TPostgresCompiler
uses Daf.SQLCute.Compiler.MySql; // TMySqlCompiler
// … etc.
var Compiler: IQueryCompiler := TPostgresCompiler.Create; // contado por referencia: ha de tener dueño
var R := Compiler.Compile(Q);También puedes usar el atajo Q.Compile(MiCompilador) cuando tienes una instancia del compilador.
Clone — Variantes de Consulta
Clone crea una copia profunda, útil para construir consultas relacionadas a partir de una base común:
var Base := TQuery.New.From('pedidos').Where('año', 2024);
var Pendientes := Base.Clone.Where('estado', 'pendiente');
var Enviados := Base.Clone.Where('estado', 'enviado');Seguridad con NULL
Todos los métodos Where* que reciben un Variant aceptan valores Null. Cuando el valor es Null, el SQL generado usa IS NULL con = o sin operador, e IS NOT NULL con cualquier otro, sin enlace; WhereNot conserva su negación.
.Where('eliminado_en', Null) // → WHERE eliminado_en IS NULL
.WhereNot('eliminado_en', Null) // → WHERE NOT (eliminado_en IS NULL)Diferencias con SqlKata
SQLCute sigue el constructor y los compiladores de SqlKata. Estos son los puntos en que el SQL que emite, o lo que acepta, es distinto:
| Caso | SqlKata | SQLCute |
|---|---|---|
| LIKE insensible a mayúsculas en Postgres | ILIKE |
LOWER(columna) LIKE ?, como en todos los dialectos |
| Marcadores | @p0, @p1… en todos los dialectos, y :p0… en Oracle |
? en todos los dialectos; en el estilo nativo, $1… en Postgres, @p0… en SQL Server, :p1… en Oracle, ? en los demás |
| Compilador base | entrecomilla los identificadores con " |
TAnsiSqlCompiler no entrecomilla; cada compilador de dialecto entrecomilla con su carácter |
WhereIn con un conjunto vacío |
1 = 0 |
col IS NULL AND col IS NOT NULL, que nombra la columna |
Where(col, 'in', valores) |
lo rechaza | liga los valores, como WhereIn |
WhereNot con Null o con un valor lógico |
pierde la negación | la conserva: NOT (col IS NULL) |
| Una comparación negada con un valor lógico | col != true |
NOT (col = true) |
| Un valor lógico en Firebird | col = 1, que una columna BOOLEAN rechaza |
col = true, que una columna SMALLINT que hace de indicador rechaza |
| Carácter de escape de la familia LIKE | cualquier carácter | una comilla simple o un ? lanzan EArgumentException |
\? en una expresión en bruto |
queda \? en el SQL |
pasa a ser ??, el ? literal de FireDAC, fuera de una cadena, y ? dentro de una y en el estilo nativo |
Una página de SQL Server sobre una consulta Distinct sin OrderBy |
ORDER BY (SELECT 0), que SQL Server rechaza con DISTINCT |
ORDER BY 1 |
Ver También
SELECT & FROM
SELECT & FROM
🌍 Idioma: English | Español
From
Establece la tabla fuente. Todas las consultas empiezan aquí.
TQuery.New.From('pedidos')
// → SELECT * FROM pedidosCon alias:
TQuery.New.From('pedidos', 'p')
// → SELECT * FROM pedidos pSelect — Lista de Columnas
TQuery.New.From('usuarios')
.Select(['id', 'nombre', 'email'])
// → SELECT id, nombre, email FROM usuariosColumna única:
.Select('nombre')
// → SELECT nombre FROM usuariosCon alias:
.SelectAs('nombre_completo', 'nombre')
// → SELECT nombre_completo AS nombre FROM usuariosSELECT * (por defecto)
Cuando no hay llamada a Select, se emite SELECT *:
TQuery.New.From('productos')
// → SELECT * FROM productosDistinct
TQuery.New.From('pedidos')
.Select('cliente_id')
.Distinct
// → SELECT DISTINCT cliente_id FROM pedidosSelectRaw
Emite una expresión SQL cruda en la lista SELECT (sin entrecomillado ni escape):
TQuery.New.From('ventas')
.SelectRaw('YEAR(creado_en) AS año')
.SelectRaw('COUNT(*) AS total')
// → SELECT YEAR(creado_en) AS año, COUNT(*) AS total FROM ventasFunciones de Agregado
| Método | Alias por defecto | SQL generado |
|---|---|---|
SelectCount(col, alias) |
'count' |
COUNT(col) AS alias |
SelectSum(col, alias) |
'sum' |
SUM(col) AS alias |
SelectAvg(col, alias) |
'avg' |
AVG(col) AS alias |
SelectMin(col, alias) |
'min' |
MIN(col) AS alias |
SelectMax(col, alias) |
'max' |
MAX(col) AS alias |
TQuery.New.From('pedidos')
.SelectCount('*', 'total_pedidos')
.SelectSum('importe', 'ingresos')
.SelectAvg('importe', 'pedido_medio')
.GroupBy('estado')
// → SELECT COUNT(*) AS total_pedidos, SUM(importe) AS ingresos, AVG(importe) AS pedido_medio
// FROM pedidos GROUP BY estadoORDER BY
.OrderBy('nombre') // ASC
.OrderByDesc('creado_en') // DESC
.OrderByRaw('FIELD(estado, ''pendiente'', ''enviado'', ''cerrado'')')Múltiples ORDER BY — llama a OrderBy/OrderByDesc varias veces:
TQuery.New.From('productos')
.OrderBy('categoria')
.OrderByDesc('precio')
// → SELECT * FROM productos ORDER BY categoria ASC, precio DESCDónde caen los NULL
Un OrderBy sobre una columna nullable no dice nada de los NULL, así que cada base de datos aplica su propia regla: Postgres y Oracle los ordenan al final en ascendente; MySQL, SQL Server, SQLite y Firebird los ponen al principio. Cuando importa, dilo explícitamente con NullsFirst / NullsLast:
TQuery.New.From('personas').OrderBy('edad').NullsLast
// → SELECT * FROM personas ORDER BY edad ASC NULLS LAST
TQuery.New.From('personas').OrderByDesc('edad').NullsFirst
// → SELECT * FROM personas ORDER BY edad DESC NULLS FIRSTEl modificador cualifica la última columna añadida al ORDER BY, de modo que cada columna conserva su propia colocación:
TQuery.New.From('personas')
.OrderBy('edad').NullsLast
.OrderBy('nombre')
// → SELECT * FROM personas ORDER BY edad ASC NULLS LAST, nombre ASCLo mismo en una sola llamada, cuando pasar la opción se lee mejor que encadenar:
TQuery.New.From('personas').OrderBy('edad', TNullsOrder.Last) // Daf.SQLCute.Clauses
TQuery.New.From('personas').OrderByDesc('edad', TNullsOrder.First)NullsFirst / NullsLast lanzan excepción si no hay columna ordenada que cualificar — una expresión OrderByRaw no cuenta, porque se emite tal cual.
No todos los dialectos tienen la sintaxis NULLS FIRST/LAST; los que no la tienen la emulan, así que una ordenación cualificada significa lo mismo en todas partes. Consulta Compiladores de Dialecto para el detalle por dialecto y el valor por omisión de cada base de datos.
LIMIT y OFFSET
TQuery.New.From('logs').Limit(100)
// → SELECT * FROM logs LIMIT 100
TQuery.New.From('logs').Limit(20).Offset(40)
// → SELECT * FROM logs LIMIT 20 OFFSET 40Take y Skip son alias de Limit y Offset:
.Take(20).Skip(40) // igual que Limit(20).Offset(40)Para paginación basada en páginas, consulta Operaciones de Conjunto.
WHERE
WHERE
🌍 Idioma: English | Español
Igualdad Básica
.Where('estado', 'activo')
// → WHERE estado = ? Bindings: ['activo']Con operador explícito:
.Where('precio', '>', 100)
.Where('stock', '<=', 0)
// → WHERE precio > ? AND stock <= ? Bindings: [100, 0]El operador es uno de =, <>, !=, <, <=, >, >=, like, not like, in y not in, en mayúsculas o minúsculas; cualquier otro lanza EArgumentException.
Conector OR:
.Where('estado', 'pendiente')
.OrWhere('estado', 'procesando')
// → WHERE estado = ? OR estado = ?WhereNot
.WhereNot('rol', 'invitado')
// → WHERE NOT (rol = ?)
.OrWhereNot('estado', 'cerrado')
// → OR NOT (estado = ?)Comprobaciones NULL
.WhereNull('eliminado_en') // → WHERE eliminado_en IS NULL
.WhereNotNull('email') // → WHERE email IS NOT NULL
.OrWhereNull('archivado_en') // → OR archivado_en IS NULLPasar Null directamente a Where, OrWhere, WhereNot u OrWhereNot compara con NULL como SqlKata: con = o sin operador emite IS NULL, y con cualquier otro operador IS NOT NULL, sin enlace. WhereNot conserva su negación, que SqlKata pierde:
.Where('eliminado_en', Null) // → WHERE eliminado_en IS NULL
.Where('eliminado_en', '<>', Null) // → WHERE eliminado_en IS NOT NULL
.WhereNot('eliminado_en', Null) // → WHERE NOT (eliminado_en IS NULL)Abreviatura Booleana
.WhereTrue('activo') // → WHERE activo = true
.WhereFalse('bloqueado') // → WHERE bloqueado = falseLa comparación es con el literal del dialecto, sin enlace: true y false en ANSI, Postgres, MySQL, Oracle y Firebird, 1 y 0 en SQLite, y cast(1 as bit) y cast(0 as bit) en SQL Server —ver Compiladores de dialecto—. Where, OrWhere, WhereNot y OrWhereNot con un valor lógico emiten lo mismo: con = o sin operador, la comparación con el literal; con cualquier otro operador, su negación, que se suma a la de WhereNot:
.Where('activo', True) // → WHERE activo = true
.Where('activo', '<>', False) // → WHERE NOT (activo = false)BETWEEN
.WhereBetween('precio', 10, 100)
// → WHERE precio BETWEEN ? AND ? Bindings: [10, 100]
.WhereNotBetween('edad', 13, 17)
.OrWhereBetween('puntuacion', 90, 100)WHERE IN
.WhereIn('estado', ['pendiente', 'procesando', 'enviado'])
// → WHERE estado IN (?, ?, ?)
.WhereNotIn('id', [1, 2, 3])
.OrWhereIn('categoria_id', [10, 20])
.OrWhereNotIn('etiqueta', ['spam', 'bot'])El conjunto vacío
Un conjunto calculado puede salir vacío, e IN sobre nada tiene respuesta: 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 NOT IN.
Los cuatro verbos resuelven ese caso al añadir la condición, de modo que nunca se emite un IN ():
.WhereIn('precio', [])
// → WHERE (precio IS NULL AND precio IS NOT NULL) no casa ninguna fila
.WhereNotIn('precio', [])
// → WHERE NOT (precio IS NULL AND precio IS NOT NULL) casa todas las filasIS NULL e IS NOT NULL son los únicos predicados del modelo que nunca dan desconocido: exactamente uno de los dos se cumple para cualquier valor. Eso es lo que hace el grupo falso para todas las filas incluida aquella cuya columna es NULL, donde precio <> precio o precio = NULL darían desconocido y la descartarían en silencio. Además nombra la columna, así que una sentencia registrada apunta de vuelta a la llamada que la produjo — cosa que 1 = 0 no haría.
Los dos back-ends leen las mismas cláusulas, así que el lane SQL y el lane en memoria responden igual.
WHERE EXISTS (subconsulta)
var Sub := TQuery.New.From('lineas_pedido').WhereColumns('pedido_id', '=', 'pedidos.id');
TQuery.New.From('pedidos')
.WhereExists(Sub)
// → WHERE EXISTS (SELECT * FROM lineas_pedido WHERE pedido_id = pedidos.id)WHERE IN (subconsulta)
var UsuariosActivos := TQuery.New.From('usuarios').Select('id').Where('activo', True);
TQuery.New.From('pedidos')
.WhereInQuery('cliente_id', UsuariosActivos)
// → WHERE cliente_id IN (SELECT id FROM usuarios WHERE activo = true)WhereRaw
Añade un fragmento SQL crudo (conector AND). Usar con precaución — sin escape aplicado:
.WhereRaw('creado_en > NOW() - INTERVAL ''7 days''')WHERE Agrupado (Callback)
Envuelve condiciones en paréntesis usando un callback:
TQuery.New.From('productos')
.Where('activo', True)
.Where(function(Q: IQuery): IQuery
begin
Result := Q.Where('stock', '>', 0)
.OrWhere('pedido_pendiente_permitido', True);
end)
// → WHERE activo = true AND (stock > ? OR pedido_pendiente_permitido = true)Grupo con conector OR:
.OrWhere(function(Q: IQuery): IQuery
begin
Result := Q.Where('rol', 'admin').OrWhere('rol', 'moderador');
end)
// → OR (rol = ? OR rol = ?)WHERE Agrupado Negado (Callback)
WhereNot y OrWhereNot también aceptan un callback. El grupo se construye igual que con Where / OrWhere y después se envuelve en NOT (…):
TQuery.New.From('pedidos')
.Where('activo', True)
.WhereNot(function(Q: IQuery): IQuery
begin
Result := Q.Where('estado', 'cancelado')
.Where('reembolsado', True);
end)
// → WHERE activo = true AND NOT (estado = ? AND reembolsado = true)Grupo con conector OR:
.OrWhereNot(function(Q: IQuery): IQuery
begin
Result := Q.Where('rol', 'invitado').OrWhere('bloqueado', True);
end)
// → OR NOT (rol = ? OR bloqueado = true)⚠️ La negación se aplica al grupo entero, no a cada condición.
NOT (estado = ? AND reembolsado = true)se cumple cuando falla cualquiera de las condiciones internas — un pedido cancelado pero no reembolsado sigue cumpliendo. Si lo que querías es “ninguna de las dos”, niega cada condición:.WhereNot('estado', 'cancelado').WhereNot('reembolsado', True).
La negación afecta solo a su propio subárbol: ni las condiciones hermanas ni el grupo que lo contiene se ven alterados.
.WhereNot(function(Q: IQuery): IQuery
begin
Result := Q.Where('a', 1);
end)
.Where('b', 2)
// → WHERE NOT (a = ?) AND b = ? — b no queda negadaPara negar una sola condición usa la forma de hoja. Es la idiomática y produce el mismo SQL que un grupo de un solo elemento:
.WhereNot('rol', 'invitado') // → WHERE NOT (rol = ?) ← idiomáticaUn grupo negado cuyo callback no añade ninguna condición se omite de la consulta, igual que un grupo sin negar vacío — nunca se emite NOT ():
.Where('a', 1)
.WhereNot(function(Q: IQuery): IQuery begin Result := Q end)
// → WHERE a = ?Bajo la lógica trivaluada de SQL, un grupo negado que contiene una comparación contra NULL se evalúa como desconocido, así que la fila queda excluida — la misma regla que NOT ya aplica sobre una condición suelta. El back-end en memoria la aplica de forma idéntica.
Precedencia, y el árbol que hay detrás
AND liga más fuerte que OR. Una cadena plana de condiciones no se lee de izquierda a derecha: primero se agrupan las tiradas consecutivas de AND, y esos grupos se unen luego con OR.
Query.From('users')
.Where('active', '=', True)
.Where('age', '>', 18)
.OrWhere('role', '=', 'admin')
// -> WHERE (active = true AND age > ?) OR role = ?Los paréntesis de ese SQL se emiten, no se dan por supuestos. Cada condición lleva el conector que la une con la anterior, y Daf.SQLCute.WhereTree normaliza esa lista plana en un árbol booleano con la precedencia correcta antes de compilar nada:
uses Daf.SQLCute.WhereTree;
var Tree := TWhereTree.Build(Query.Clauses);
try
// Tree.Kind es TWhereNodeKind.Disjunction para la consulta de arriba
finally
Tree.Free;
end;| Miembro | Responde |
|---|---|
TWhereTree.Build(Clauses) |
El árbol de las cláusulas que afectan al WHERE. nil cuando no hay ninguna. Quien lo pide lo posee y lo libera |
TWhereTree.BuildHaving(Clauses) |
La misma normalización sobre THavingClause. HAVING no tiene anidamiento ni NOT, así que el árbol es una agrupación AND/OR plana |
TWhereNode.Kind |
TWhereNodeKind.Leaf, .Neg, .Conjunction o .Disjunction — la unit compila con enumerados con ámbito |
TWhereNode.Clause |
La condición, en un Leaf. No la posee: apunta de vuelta al AST de la consulta |
TWhereNode.Children |
Los operandos de una conjunción o disyunción, o el único hijo de un Neg. Los posee, y los libera recursivamente |
Un grupo por callback baja como un nodo anidado, y uno negado envuelve su nodo en un Neg.
Los dos back-ends consumen el mismo árbol, y eso es el punto. El compilador ANSI lo recorre y escribe los paréntesis, así que la precedencia deja de depender de cómo parsee una base de datos concreta; el compilador en memoria construye a partir de él su predicado de tres valores. Dos motores que leen un solo árbol no pueden discrepar sobre la precedencia — ver SQLCute.InMemory.
El recorrido es en profundidad y de izquierda a derecha, lo que conserva el orden original de las condiciones. Es lo que permite que un consumidor emita bindings posicionales mientras recorre y obtenga el mismo orden que producía la lista plana.
WhereColumns
Comparación columna a columna (sin binding — los valores se emiten literalmente):
.WhereColumns('fecha_inicio', '<', 'fecha_fin')
// → WHERE fecha_inicio < fecha_fin
.OrWhereColumns('precio', '=', 'precio_lista')Su operador es uno de =, <>, !=, <, <=, >, >=, like y not like; in y not in lanzan EArgumentException.
⚠️ Pasa solo nombres de columnas controlados por el desarrollador a
WhereColumns. Nunca pases input del usuario.
When — Encadenamiento Condicional
Aplica condiciones solo cuando un flag en tiempo de ejecución es verdadero:
var IncluirInactivos := False;
TQuery.New.From('usuarios')
.When(not IncluirInactivos, function(Q: IQuery): IQuery
begin
Result := Q.Where('activo', True);
end)
// → WHERE activo = true (solo cuando IncluirInactivos es False)Con rama else:
.When(EsAdmin,
function(Q: IQuery): IQuery begin Result := Q; end, // sin filtro extra
function(Q: IQuery): IQuery begin Result := Q.Where('tenant_id', TenantId); end)Condiciones de Cadena / LIKE
Consulta Operaciones de Cadena para WhereLike, WhereStarts, WhereEnds, WhereContains.
Condiciones de Fecha
Consulta Operaciones de Fecha para WhereDate, WhereTime, WhereDatePart.
JOINs
JOINs
🌍 Idioma: English | Español
Joins Básicos
Todos los métodos de join siguen la misma firma: TipoJoin(tabla, col1, col2, op). El operador por defecto es =, y es uno de =, <>, !=, <, <=, >, >=, like y not like; cualquier otro lanza EArgumentException.
INNER JOIN
TQuery.New.From('pedidos')
.Join('clientes', 'pedidos.cliente_id', 'clientes.id')
// → SELECT * FROM pedidos INNER JOIN clientes ON pedidos.cliente_id = clientes.idLEFT JOIN
.LeftJoin('direcciones', 'usuarios.id', 'direcciones.usuario_id')
// → LEFT JOIN direcciones ON usuarios.id = direcciones.usuario_idRIGHT JOIN
.RightJoin('departamentos', 'empleados.dept_id', 'departamentos.id')
// → RIGHT JOIN departamentos ON empleados.dept_id = departamentos.idCROSS JOIN
.CrossJoin('tallas')
// → CROSS JOIN tallasFULL OUTER JOIN
.FullOuterJoin('b', 'a.id', 'b.a_id')
// → FULL OUTER JOIN b ON a.id = b.a_idJoin con Cadena de Condición Cruda
Pasa una cláusula ON de SQL crudo cuando la condición no puede expresarse como columna=columna:
.Join('precios', 'precios.producto_id = productos.id AND precios.activo = 1')
// → INNER JOIN precios ON precios.producto_id = productos.id AND precios.activo = 1Join con Callback (ON Complejo)
Usa un callback para múltiples condiciones unidas con AND/OR:
TQuery.New.From('pedidos')
.Join('lineas_pedido', function(Q: IQuery): IQuery
begin
Result := Q
.WhereColumns('lineas_pedido.pedido_id', '=', 'pedidos.id')
.OrWhereColumns('lineas_pedido.pedido_alt_id', '=', 'pedidos.id');
end)
// → INNER JOIN lineas_pedido ON (lineas_pedido.pedido_id = pedidos.id
// OR lineas_pedido.pedido_alt_id = pedidos.id)En el callback, una columna al otro lado va por WhereColumns; Where liga su valor como parámetro.
LeftJoin, RightJoin y FullOuterJoin admiten el mismo overload de callback.
Join con Subconsulta
var Sub := TQuery.New.From('lineas_pedido')
.Select('pedido_id')
.SelectSum('qty', 'qty_total')
.GroupBy('pedido_id');
TQuery.New.From('pedidos')
.Join(Sub, 'li', function(Q: IQuery): IQuery
begin
Result := Q.WhereColumns('li.pedido_id', '=', 'pedidos.id');
end)
// → INNER JOIN (SELECT pedido_id, SUM(qty) AS qty_total
// FROM lineas_pedido GROUP BY pedido_id) AS li
// ON (li.pedido_id = pedidos.id)Múltiples Joins
Encadena tantos joins como necesites — se emiten en orden de declaración:
TQuery.New.From('facturas')
.Join('clientes', 'facturas.cliente_id', 'clientes.id')
.LeftJoin('descuentos', 'facturas.descuento_id', 'descuentos.id')
.Select(['facturas.id', 'clientes.nombre', 'descuentos.pct'])GROUP BY y Agregados
GROUP BY / HAVING
🌍 Idioma: English | Español
GROUP BY
TQuery.New.From('ventas')
.SelectCount('*', 'qty')
.GroupBy('producto_id')
// → SELECT COUNT(*) AS qty FROM ventas GROUP BY producto_idMúltiples columnas — llama a GroupBy varias veces o pasa un array:
.GroupBy(['año', 'mes', 'categoria'])
// → GROUP BY año, mes, categoriaExpresión cruda:
.GroupByRaw('YEAR(creado_en), MONTH(creado_en)')
// → GROUP BY YEAR(creado_en), MONTH(creado_en)HAVING
Una condición sobre un agregado va por HavingRaw, que aceptan todos los motores:
TQuery.New.From('pedidos')
.GroupBy('cliente_id')
.SelectCount('*', 'num_pedidos')
.SelectSum('total', 'ingresos')
.HavingRaw('count(*) > 5')
.HavingRaw('sum(total) >= 1000')
// → SELECT COUNT(*) AS num_pedidos, SUM(total) AS ingresos
// FROM pedidos
// GROUP BY cliente_id
// HAVING count(*) > 5 AND sum(total) >= 1000Having compara una columna con un valor ligado. La columna nombra una columna agrupada o el alias de una columna de salida, y se entrecomilla como cualquier otra columna del dialecto. Su operador es uno de =, <>, !=, <, <=, >, >=, like y not like; in y not in lanzan EArgumentException, y un IN sobre un agregado va por HavingRaw. MySQL, SQLite y Oracle 23 aceptan el alias de una columna de salida en HAVING; Postgres, SQL Server y Firebird lo rechazan.
.Having('num_pedidos', '>', 5)
// Postgres → HAVING "num_pedidos" > ?Conector OR:
.Having('num_pedidos', '>', 10)
.OrHaving('ingresos', '>=', 1000)
// → HAVING num_pedidos > ? OR ingresos >= ?Expresión HAVING cruda:
.HavingRaw('AVG(tiempo_respuesta) < 200')Having con Null compara igual que Where: IS NULL con = e IS NOT NULL con cualquier otro operador, sin enlace.
Combinando con WHERE
WHERE filtra filas antes de agrupar; HAVING filtra grupos después:
TQuery.New.From('logs')
.Where('nivel', 'error') // aplicado antes de GROUP BY
.GroupBy('servicio')
.SelectCount('*', 'errores')
.HavingRaw('count(*) > 50') // aplicado después de GROUP BY
// → SELECT COUNT(*) AS errores FROM logs
// WHERE nivel = ?
// GROUP BY servicio
// HAVING count(*) > 50Operaciones de Conjunto
Operaciones de Conjunto y Paginación
🌍 Idioma: English | Español
UNION
var Q1 := TQuery.New.From('usuarios_activos').Select(['id', 'nombre']);
var Q2 := TQuery.New.From('usuarios_archivados').Select(['id', 'nombre']);
Q1.Union(Q2)
// → SELECT id, nombre FROM usuarios_activos
// UNION
// SELECT id, nombre FROM usuarios_archivadosUNION ALL (mantiene duplicados):
Q1.UnionAll(Q2)
// → … UNION ALL …INTERSECT / EXCEPT
Q1.Intersect(Q2)
// → … INTERSECT …
Q1.&Except(Q2) // & para evitar conflicto con keyword de Delphi
// → … EXCEPT …
Q1.IntersectAll(Q2)
Q1.ExceptAll(Q2)CombineRaw
Añade una operación de conjunto cruda:
Q1.CombineRaw('MINUS SELECT id, nombre FROM usuarios_eliminados')
// → … MINUS SELECT id, nombre FROM usuarios_eliminadosPaginación
ForPage (recomendado)
Número de página base-1, tamaño de página configurable:
TQuery.New.From('productos')
.Where('activo', True)
.OrderBy('nombre')
.ForPage(3, 25) // página 3, 25 elementos por página
// → … LIMIT 25 OFFSET 50Tamaño de página por defecto: 15:
.ForPage(2) // → LIMIT 15 OFFSET 15LIMIT / OFFSET Manual
.Limit(20).Offset(40)
// → LIMIT 20 OFFSET 40
// Alias
.Take(20).Skip(40)Bucle típico de paginación
const TamanoPagina = 50;
var Pagina := 1;
repeat
var R := Compilador.Compile(
TQuery.New.From('pedidos').OrderBy('id').ForPage(Pagina, TamanoPagina));
// ejecutar R, procesar filas…
Inc(Pagina);
until NumFilas < TamanoPagina;DML
DML — INSERT / UPDATE / DELETE
🌍 Idioma: English | Español
INSERT — Fila Única
Pasa arrays paralelos de nombres de columna y valores:
TQuery.New.From('usuarios')
.AsInsert(
['nombre', 'email', 'rol'],
['Alicia', 'alicia@ejemplo.com', 'admin'])
// → INSERT INTO usuarios (nombre, email, rol) VALUES (?, ?, ?)
// Bindings: ['Alicia', 'alicia@ejemplo.com', 'admin']INSERT — Múltiples Filas
TQuery.New.From('etiquetas')
.AsInsertRows(
['nombre', 'color'],
[['Urgente', 'rojo'],
['Normal', 'verde'],
['Bajo', 'gris']])
// → INSERT INTO etiquetas (nombre, color) VALUES (?, ?), (?, ?), (?, ?)
// Bindings: ['Urgente', 'rojo', 'Normal', 'verde', 'Bajo', 'gris']INSERT … SELECT
Copia filas de una tabla a otra usando una subconsulta:
var Sub := TQuery.New.From('usuarios_temp')
.Select(['nombre', 'email'])
.Where('importado', True);
TQuery.New.From('usuarios')
.AsInsertFrom(['nombre', 'email'], Sub)
// → INSERT INTO usuarios (nombre, email)
// SELECT nombre, email FROM usuarios_temp WHERE importado = trueUPDATE
TQuery.New.From('productos')
.Where('id', 42)
.AsUpdate(
['precio', 'stock'],
[29.99, 100])
// → UPDATE productos SET precio = ?, stock = ? WHERE id = ?
// Bindings: [29.99, 100, 42]⚠️ Sin cláusula
Where, se actualizan TODAS las filas. Añade una condición WHERE para acotar la actualización.
DELETE
TQuery.New.From('sesiones')
.Where('expirado', True)
.AsDelete
// → DELETE FROM sesiones WHERE expirado = truePatrón de borrado lógico usando UPDATE:
TQuery.New.From('usuarios')
.Where('id', IdUsuario)
.AsUpdate(['eliminado_en'], [Now])
// → UPDATE usuarios SET eliminado_en = ? WHERE id = ?Combinando DML con Subconsultas
// Eliminar pedidos con más de 1 año cuyas líneas están todas enviadas
TQuery.New.From('pedidos')
.Where('creado_en', '<', EncodeDate(Año - 1, Mes, Dia))
.WhereNotExists(
TQuery.New.From('lineas_pedido')
.WhereColumns('pedido_id', '=', 'pedidos.id')
.WhereNot('estado', 'enviado'))
.AsDeleteSubconsultas
Subconsultas y CTEs
🌍 Idioma: English | Español
Tabla Derivada (FROM con subconsulta)
Usa un IQuery como fuente FROM con un alias:
var Interno := TQuery.New.From('lineas_pedido')
.Select('pedido_id')
.SelectSum('qty', 'qty_total')
.GroupBy('pedido_id');
TQuery.New
.From(Interno, 'li')
.Select(['li.pedido_id', 'li.qty_total'])
.Where('li.qty_total', '>', 10)
// → SELECT li.pedido_id, li.qty_total
// FROM (SELECT pedido_id, SUM(qty) AS qty_total
// FROM lineas_pedido GROUP BY pedido_id) li
// WHERE li.qty_total > ?FROM Raw
Usa una expresión SQL cruda como fuente FROM (p.ej., funciones con valores de tabla):
TQuery.New
.FromRaw('generate_series(1, 100) AS s(n)', [])
.Select('n')
// → SELECT n FROM generate_series(1, 100) AS s(n)Con bindings:
.FromRaw('fn_obtener_eventos(?, ?) AS e', [FechaInicio, FechaFin])Sus ? cuentan con los marcadores del resto de la consulta, y sus enlaces ocupan ese mismo lugar: en el estilo nativo de Postgres, $1 y $2 aquí, y el siguiente de la consulta $3.
Subconsulta en WHERE IN
var PedidosRecientes := TQuery.New.From('pedidos')
.Select('cliente_id')
.Where('creado_en', '>', SemanaAnterior);
TQuery.New.From('clientes')
.WhereInQuery('id', PedidosRecientes)
// → SELECT * FROM clientes
// WHERE id IN (SELECT cliente_id FROM pedidos WHERE creado_en > ?)Variantes: WhereNotInQuery, OrWhereInQuery, OrWhereNotInQuery.
Subconsulta en WHERE EXISTS
TQuery.New.From('pedidos')
.WhereExists(
TQuery.New.From('pagos')
.WhereColumns('pedido_id', '=', 'pedidos.id')
.Where('estado', 'completo'))
// → WHERE EXISTS (SELECT * FROM pagos
// WHERE pedido_id = pedidos.id AND estado = ?)La columna de la consulta de fuera va por WhereColumns; Where liga su valor como parámetro.
Subconsulta Escalar en SELECT
var NumLineas := TQuery.New.From('lineas_pedido')
.SelectCount('*')
.WhereColumns('pedido_id', '=', 'pedidos.id');
TQuery.New.From('pedidos')
.Select(NumLineas, 'num_lineas')
// → SELECT (SELECT COUNT(*) AS count FROM lineas_pedido WHERE pedido_id = pedidos.id)
// AS num_lineas FROM pedidosWITH (CTE)
var VentasRecientes := TQuery.New.From('ventas')
.Where('año', 2024)
.Select(['producto_id', 'importe']);
TQuery.New
.&With('recientes', VentasRecientes)
.From('recientes')
.SelectSum('importe', 'total')
// → WITH recientes AS (SELECT producto_id, importe FROM ventas WHERE año = ?)
// SELECT SUM(importe) AS total FROM recientesCTE RECURSIVO
var Ancla := TQuery.New.From('categorias')
.Where('padre_id', Null);
var Recursivo := TQuery.New.From('categorias')
.Join('arbol', 'categorias.padre_id', 'arbol.id');
TQuery.New
.WithRecursive('arbol', Ancla.Union(Recursivo))
.From('arbol')
// → WITH RECURSIVE arbol AS (
// SELECT * FROM categorias WHERE padre_id IS NULL
// UNION
// SELECT categorias.* FROM categorias INNER JOIN arbol ON categorias.padre_id = arbol.id
// ) SELECT * FROM arbolWITH Raw
TQuery.New
.WithRaw('nombre_cte', 'SELECT id FROM tabla_legacy WHERE flag = ?', [1])
.From('nombre_cte')Sus ? cuentan con los marcadores del resto de la consulta, en el orden del texto.
Profundidad de Anidamiento
SQLCute admite anidamiento arbitrario — las subconsultas pueden contener a su vez otras subconsultas. Los bindings se aplanan en un único array en el orden en que aparecen en el SQL compilado.
Operaciones de Cadena
Operaciones de Cadena
🌍 Idioma: English | Español
Resumen
Todos los métodos de coincidencia de cadenas envuelven el valor en patrones LIKE. Por defecto (CaseSensitive = False), SQLCute envuelve la columna en LOWER(…) y pasa el patrón a minúsculas, que viaja como binding. Pasa True como CaseSensitive, el argumento que sigue al valor, para emitir un LIKE llano con el patrón tal y como lo escribiste.
Los comodines % y _ dentro del valor siguen activos en todos los métodos: WhereContains('codigo', '50%') casa con cualquier código que contenga 50. Para casarlos como literales, declara un carácter de escape.
WhereLike — Patrón Libre
Proporciona el patrón LIKE completo:
.WhereLike('nombre', '%Juan%')
// Insensible a mayúsculas (por defecto):
// → WHERE LOWER(nombre) LIKE ? binding: '%juan%'
.WhereLike('codigo', 'ABC-%', True)
// Sensible a mayúsculas:
// → WHERE codigo LIKE ? binding: 'ABC-%'Variantes: WhereNotLike, OrWhereLike, OrWhereNotLike.
WhereStarts — Coincidencia de Prefijo
.WhereStarts('email', 'admin')
// → WHERE LOWER(email) LIKE ? binding: 'admin%'Variantes: WhereNotStarts, OrWhereStarts, OrWhereNotStarts.
WhereEnds — Coincidencia de Sufijo
.WhereEnds('archivo', '.pdf')
// → WHERE LOWER(archivo) LIKE ? binding: '%.pdf'Variantes: WhereNotEnds, OrWhereEnds, OrWhereNotEnds.
WhereContains — Coincidencia de Subcadena
.WhereContains('descripcion', 'urgente')
// → WHERE LOWER(descripcion) LIKE ? binding: '%urgente%'Variantes: WhereNotContains, OrWhereContains, OrWhereNotContains.
Carácter de escape
Todos los métodos de la familia aceptan un carácter de escape opcional tras CaseSensitive. El carácter que lo sigue en el valor es literal, y el SQL añade ESCAPE '<c>':
.WhereContains('codigo', '50!%', False, '!')
// → WHERE LOWER(codigo) LIKE ? ESCAPE '!' binding: '%50!%%'SQLCute no escapa nada por su cuenta: los comodines que añade alrededor del valor siguen siendo comodines, y solo es literal lo que precede el carácter de escape. Una comilla simple o un ? como carácter de escape lanzan EArgumentException.
Con CaseSensitive = False el patrón pasa a minúsculas y el carácter de escape no, así que elige uno sin mayúscula, como !. El back-end en memoria lee el carácter de escape igual.
Combinando Filtros de Cadena
TQuery.New.From('productos')
.Where(function(Q: IQuery): IQuery
begin
Result := Q.WhereContains('nombre', 'cafe')
.OrWhereContains('etiquetas', 'cafe');
end)
.WhereNotStarts('sku', 'DESC-')
// → WHERE (LOWER(nombre) LIKE ? OR LOWER(etiquetas) LIKE ?)
// AND LOWER(sku) NOT LIKE ?
// bindings: '%cafe%', '%cafe%', 'desc-%'Encadenar los dos términos OR sin ese grupo significa otra cosa: una lista plana se lee como OR-de-tramos-AND, así que WhereContains … OrWhereContains … WhereNotStarts agrupa como nombre OR (etiquetas AND sku). Anida la alternancia que quieres decir.
Notas sobre Sensibilidad a Mayúsculas por Dialecto
El patrón pasa a minúsculas en todo Unicode, así que WhereLike('nombre', 'ÑANDÚ%') enlaza 'ñandú%'. La columna la pasa a minúsculas el LOWER del motor, y el de SQLite solo pliega las letras ASCII: en SQLite, WhereStarts('nombre', 'ñan') no casa con un Ñandú guardado. El back-end en memoria pliega los dos lados en todo Unicode.
Cada base de datos tiene además su propio comportamiento nativo de LIKE:
| Dialecto | ¿LIKE sensible a mayúsculas? | Notas |
|---|---|---|
| ANSI SQL | Sí | Comportamiento estándar |
| PostgreSQL | Sí | Admite ILIKE nativamente; SQLCute usa LOWER(…) |
| MySQL | No (por defecto) | El envoltorio LOWER no tiene efecto negativo |
| SQLite | No para ASCII | Las letras no ASCII son sensibles, y su LOWER solo pliega ASCII, sin la extensión ICU |
| SQL Server | Depende de la intercalación | Latin1_General_CI_AS → insensible a mayúsculas |
| Oracle | Sí | Se recomienda el envoltorio LOWER |
| Firebird | Sí | Se recomienda el envoltorio LOWER |
Cuando CaseSensitive = True, no se emite ningún envoltorio LOWER independientemente del dialecto.
Operaciones de Fecha
Operaciones de Fecha
🌍 Idioma: English | Español
WhereDate — Coincidencia de Fecha Exacta
Filtra filas donde la parte DATE de una columna datetime es igual a un valor, ignorando el componente de tiempo. La expresión de conversión es específica del dialecto (ver tabla más abajo).
TQuery.New.From('pedidos')
.WhereDate('creado_en', '2024-06-15')
// ANSI: WHERE CAST(creado_en AS DATE) = ?
// Bindings: ['2024-06-15']Conector OR:
.OrWhereDate('actualizado_en', '2024-06-15')WhereTime — Comparación de Hora
TQuery.New.From('citas')
.WhereTime('hora_inicio', '>=', '09:00:00')
// ANSI: WHERE CAST(hora_inicio AS TIME) >= ?Variante OR: OrWhereTime. El operador es uno de =, <>, !=, <, <=, >, >=, like y not like; cualquier otro lanza EArgumentException al añadir la condición.
WhereDatePart — Extraer una Parte
Filtra por año, mes, día, hora o minuto:
uses Daf.SQLCute.Clauses; // TDatePart
TQuery.New.From('eventos')
.WhereDatePart(TDatePart.dpYear, 'fecha_evento', 2024)
.WhereDatePart(TDatePart.dpMonth, 'fecha_evento', 12)
// ANSI: WHERE EXTRACT(year FROM fecha_evento) = ? AND EXTRACT(month FROM fecha_evento) = ?Valores disponibles de TDatePart: dpDate, dpTime, dpYear, dpMonth, dpDay, dpHour, dpMinute.
Variante OR: OrWhereDatePart.
Matriz de Cast de Fecha por Dialecto
Cada compilador de dialecto sobreescribe la expresión de conversión para WhereDate:
| Dialecto | WhereDate('col', val) genera |
|---|---|
| ANSI (por defecto) | CAST(col AS DATE) = ? |
| PostgreSQL | col::date = ? |
| MySQL | DATE(col) = ? |
| SQLite | date(col) = ? |
| SQL Server | CAST(col AS date) = ? |
| Oracle | TRUNC(col) = ? |
| Firebird | CAST(col AS DATE) = ? |
Ejemplo PostgreSQL
uses Daf.SQLCute, Daf.SQLCute.Compiler.Postgres;
var Compiler: IQueryCompiler := TPostgresCompiler.Create; // contado por referencia: ha de tener dueño
var R := Compiler.Compile(
TQuery.New.From('t').WhereDate('d', '2024-01-01'));
// → WHERE "d"::date = ?Ejemplo MySQL
uses Daf.SQLCute, Daf.SQLCute.Compiler.MySql;
var Compiler: IQueryCompiler := TMySqlCompiler.Create; // contado por referencia: ha de tener dueño
var R := Compiler.Compile(
TQuery.New.From('t').WhereDate('d', '2024-01-01'));
// → WHERE DATE(`d`) = ?Matriz de DatePart por Dialecto
| Dialecto | EXTRACT año | EXTRACT mes | EXTRACT día | EXTRACT hora |
|---|---|---|---|---|
| ANSI | EXTRACT(year FROM col) |
EXTRACT(month FROM col) |
EXTRACT(day FROM col) |
EXTRACT(hour FROM col) |
| PostgreSQL | igual que ANSI | igual | igual | igual |
| MySQL | YEAR(col) |
MONTH(col) |
DAY(col) |
HOUR(col) |
| SQLite | strftime('%Y', col) |
strftime('%m', col) |
strftime('%d', col) |
strftime('%H', col) |
| SQL Server | DATEPART(year, col) |
DATEPART(month, col) |
DATEPART(day, col) |
DATEPART(hour, col) |
| Oracle | EXTRACT(YEAR FROM col) |
EXTRACT(MONTH FROM col) |
EXTRACT(DAY FROM col) |
— |
| Firebird | EXTRACT(YEAR FROM col) |
EXTRACT(MONTH FROM col) |
EXTRACT(DAY FROM col) |
EXTRACT(HOUR FROM col) |
Pasa valores de fecha como cadenas (
'2024-06-15') o como valoresTDateTimede Delphi — el driver de base de datos gestiona la conversión de tipo desde el bindingVariant.
⚠️ Estos predicados son solo de SQL. El cast es propio de cada dialecto, así que SQLCute.InMemory los rechaza con EInMemoryQueryError en vez de aproximarlos. Una consulta pensada para los dos back-ends filtra fechas con un Where corriente sobre una columna comparable.
Referencia de Dialectos
# Compiladores de Dialecto
🌍 Idioma: English | Español
Qué Hace un Compilador de Dialecto
El TAnsiSqlCompiler por defecto genera SQL ANSI genérico con marcadores ? y sin entrecomillado de identificadores. Los compiladores de dialecto lo extienden para manejar:
- Entrecomillado de identificadores — envolver nombres de tabla/columna para evitar conflictos con palabras reservadas
- Marcadores de parámetros —
?por defecto, y el del motor ($1,:p1,@p0) en el estilo nativo - Cast de fechas — expresiones específicas del dialecto para
WhereDate/WhereDatePart - Paginación —
LIMIT/OFFSETvsFETCH NEXT … ROWS ONLYvsROWS m TO n - Emisión de la ordenación de NULL —
NULLS FIRST/LASTnativo, o una emulación donde el dialecto no tiene esa sintaxis
Matriz de Dialectos
Todos los compiladores escriben ? por defecto. La columna del marcador es lo que escribe cada uno en el estilo nativo.
| Clase del compilador | Comillas | Marcador del estilo nativo | Unidad del paquete |
|---|---|---|---|
TAnsiSqlCompiler |
ninguna | ? |
Daf.SQLCute.Compiler |
TPostgresCompiler |
" |
$1, $2… |
Daf.SQLCute.Compiler.Postgres |
TMySqlCompiler |
` |
? |
Daf.SQLCute.Compiler.MySql |
TSQLiteCompiler |
" |
? |
Daf.SQLCute.Compiler.SQLite |
TSqlServerCompiler |
[…] |
@p0, @p1… |
Daf.SQLCute.Compiler.SqlServer |
TOracleCompiler |
", en mayúsculas |
:p1, :p2… |
Daf.SQLCute.Compiler.Oracle |
TFirebirdCompiler |
ninguna | ? |
Daf.SQLCute.Compiler.Firebird |
Un dialecto que entrecomilla entrecomilla por separado cada parte de un nombre cualificado —"public"."users", [users].[id]—, deja sin comillas un * final y duplica dentro de un nombre su carácter de cierre. Una columna escrita nombre as alias se separa: Select('users.name as n') compila a SELECT "users"."name" AS "n" en Postgres. Oracle escribe sus alias sin AS: FROM "USERS" "U".
Una comparación lógica —WhereTrue, WhereFalse, o Where con un valor lógico— compara con el literal del dialecto, sin enlace: true y false en ANSI, Postgres, MySQL, Oracle y Firebird, 1 y 0 en SQLite, y cast(1 as bit) y cast(0 as bit) en SQL Server. Oracle entiende true desde Oracle 23. En Firebird, una columna SMALLINT que hace de indicador rechaza = true con un error de conversión: se compara con un número, Where('flag', 1).
Todos los compiladores de dialecto viven en el paquete Daf.SQLCute, junto al TAnsiSqlCompiler del que extienden. Basta con requerir Daf.SQLCute: no hay un paquete de compiladores aparte que añadir.
Estilos de Marcador
La propiedad Placeholders de un compilador fija lo que escribe por cada enlace, y por un \? de una expresión en bruto:
TPlaceholderStyle |
Cada enlace | \? |
|---|---|---|
FireDAC, el de por defecto |
?, en todos los dialectos |
?? fuera de una cadena, ? dentro de una |
Native |
el del dialecto: $1… en Postgres, @p0… en SQL Server, :p1… en Oracle, ? en los demás |
? |
FireDAC ejecuta el estilo por defecto tal cual, en cualquier motor: liga ? y lo traduce al marcador del motor, lee ?? como un ? literal, y no busca parámetros dentro de una cadena.
Una cadena es lo que va entre comillas simples, y un '' dentro de ella no la cierra. Otras formas de cadena, como $$…$$ y E'…' de Postgres, no se siguen: un \? dentro de una sale como ??. El estilo nativo escribe ahí ?.
Para el estilo nativo, fija la propiedad antes de que la instancia compile. El compilador cuenta referencias, así que guárdalo en un IQueryCompiler:
uses
Daf.SQLCute,
Daf.SQLCute.Compiler,
Daf.SQLCute.Compiler.Postgres;
var Postgres := TPostgresCompiler.Create;
Postgres.Placeholders := TPlaceholderStyle.Native;
var Compilador: IQueryCompiler := Postgres;
var R := Compilador.Compile(TQuery.New.From('usuarios').Where('estado', 'abierto'));
// R.SQL → 'SELECT * FROM "usuarios" WHERE "estado" = $1'TPlaceholderStyle vive en Daf.SQLCute.Compiler. Cambiar Placeholders mientras otro hilo compila con la misma instancia no está soportado.
Marcadores en Expresiones en Bruto
Cada ? de una expresión en bruto —WhereRaw, FromRaw, WithRaw y las demás formas Raw— es un marcador, cuenta con los que genera la consulta, y su enlace ocupa ese mismo lugar; el estilo nativo lo numera con ellos. Para un signo de interrogación literal, como el operador ? de Postgres sobre jsonb, se escribe \?:
TQuery.New.From('docs').WhereRaw('data \? ''tag''').Where('id', 1)
// Postgres → SELECT * FROM "docs" WHERE data ?? 'tag' AND "id" = ?
// Postgres, estilo nativo → SELECT * FROM "docs" WHERE data ? 'tag' AND "id" = $1Usar un Compilador de Dialecto
uses
Daf.SQLCute,
Daf.SQLCute.Compiler.Postgres;
var Q := TQuery.New
.From('usuarios')
.Where('estado', 'abierto')
.OrderBy('nombre');
var Compilador: IQueryCompiler := TPostgresCompiler.Create;
var R := Compilador.Compile(Q);
// R.SQL → 'SELECT * FROM "usuarios" WHERE "estado" = ? ORDER BY "nombre"'
// R.Bindings → ['abierto']O usa el atajo Q.Compile(Compilador):
var R := Q.Compile(TPostgresCompiler.Create);Inyección de Dependencias
Una instancia de compilador no guarda nada entre compilaciones: sirve compilaciones sucesivas, anidadas y concurrentes. Regístrala como singleton e inyecta IQueryCompiler donde sea necesario:
// Raíz de composición
Services.AddSingleton<IQueryCompiler, TPostgresCompiler>;
// En un repositorio
type
TRepositorioPedidos = class
private
FCompilador: IQueryCompiler;
public
constructor Create(Compilador: IQueryCompiler);
function BuscarPorEstado(const Estado: string): TSQLResult;
end;
function TRepositorioPedidos.BuscarPorEstado(const Estado: string): TSQLResult;
begin
Result := FCompilador.Compile(
TQuery.New.From('pedidos').Where('estado', Estado));
end;Un compilador registrado por su tipo compila en el estilo por defecto. Para el estilo nativo, registra una instancia preparada como en Estilos de Marcador: Services.AddSingleton<IQueryCompiler>(Compilador).
Implementar un Compilador Personalizado
Extiende TAnsiSqlCompiler y sobreescribe solo lo que difiere:
uses System.SysUtils, Daf.SQLCute, Daf.SQLCute.Compiler;
type
TMiDbCompilador = class(TAnsiSqlCompiler)
protected
function WrapIdentifier(const Name: string): string; override;
function FormatPlaceholder(const Index: Integer): string; override;
end;
function TMiDbCompilador.WrapIdentifier(const Name: string): string;
begin
Result := '[' + StringReplace(Name, ']', ']]', [rfReplaceAll]) + ']';
end;
function TMiDbCompilador.FormatPlaceholder(const Index: Integer): string;
begin
Result := ':' + IntToStr(Index + 1);
end;WrapIdentifier entrecomilla un solo nombre. WrapColumn y WrapTable separan un alias y parten un nombre cualificado por cada punto, y lo llaman para cada parte; AliasKeyword es lo que une un nombre con su alias.
Cada método que emite SQL —de CompileSelect a CompileDelete, CompileWhereList, CompileDateWhere, CompileOrderByColumn— recibe la compilación, un TSqlCompilation, como primer parámetro, le añade sus enlaces con AddBinding y escribe cada marcador como ?. En el estilo nativo, una pasada final sustituye el ? de cada posición por lo que devuelve para ella FormatPlaceholder, contando desde 0.
Paginación por Dialecto
SQLCute emite LIMIT … OFFSET … por defecto. Los compiladores de SQL Server, Oracle y Firebird sobreescriben la paginación para usar su sintaxis nativa:
| Dialecto | SQL de paginación |
|---|---|
| ANSI / Postgres / MySQL / SQLite | LIMIT n OFFSET m |
| SQL Server | ORDER BY … OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| Oracle | OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| Firebird | ROWS m+1 TO m+n |
SQL Server exige un
ORDER BYconOFFSET/FETCH. Si la consulta no lo tiene,TSqlServerCompilerañadeORDER BY (SELECT 0)—ORDER BY 1en una consultaDistinct—, así que la página compila; qué filas contiene solo es estable con un.OrderBy(…)propio.
Ordenación de NULL por Dialecto
NullsFirst / NullsLast significan lo mismo en todos los dialectos. Donde la sintaxis existe, se emite; donde no, el compilador la emula con una clave de ordenación previa. Ninguna forma añade parámetros.
| Dialecto | Un NullsLast cualificado compila a |
Su propio valor por omisión para un ORDER BY col sin cualificar |
|---|---|---|
| ANSI / Postgres | col ASC NULLS LAST (nativo) |
NULL al final en ascendente, al principio en descendente (NULL ordena como el valor mayor) |
| Oracle | "COL" ASC NULLS LAST (nativo) |
NULL al final en ascendente, al principio en descendente |
| SQLite | "col" ASC NULLS LAST (nativo, requiere SQLite ≥ 3.30) |
NULL al principio en ascendente, al final en descendente (NULL es el valor menor) |
| Firebird | col ASC NULLS LAST (nativo) |
NULL al principio en ascendente, al final en descendente |
| MySQL | (`col` IS NULL) ASC, `col` ASC (emulado) |
NULL al principio en ascendente, al final en descendente |
| SQL Server | CASE WHEN [col] IS NULL THEN 1 ELSE 0 END ASC, [col] ASC (emulado) |
NULL al principio en ascendente, al final en descendente |
col IS NULL vale 1 para NULL y 0 en caso contrario, así que la clave emulada ordenada ASC retiene los NULL y ordenada DESC los adelanta. SQL Server necesita el CASE porque T-SQL no admite expresiones booleanas en ORDER BY.
Notas:
- Los valores por omisión son de las bases de datos, no de SQLCute. Un
OrderBysin cualificar no emite ninguna cláusula NULL en ningún dialecto, así que la última columna es lo que hace tu base de datos con él. Cualifica la ordenación para obtener una única respuesta en todas partes. - SQLite < 3.30 rechaza la sintaxis nativa. Como la emisión vive detrás del método sobreescribible
CompileOrderByColumn, un proyecto atado a un motor antiguo puede derivar deTSQLiteCompilery emular como hace MySQL. - Firebird no usa índice en una columna ordenada con una colocación
NULLSdistinta de la suya por omisión.