Saltar al contenido principal

# 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

  1. SELECT & FROM — columnas, DISTINCT, agregados, ORDER BY, LIMIT/OFFSET
  2. WHERE — igualdad, comparaciones, NULL, BETWEEN, IN, EXISTS, grupos, columnas
  3. JOINs — INNER, LEFT, RIGHT, CROSS, FULL OUTER, callbacks
  4. GROUP BY / HAVING — agrupación y filtros de agregados
  5. Operaciones de Conjunto — UNION, INTERSECT, EXCEPT, paginación
  6. DML — INSERT, UPDATE, DELETE
  7. Subconsultas y CTEs — tablas derivadas, WITH, CTEs recursivos
  8. Operaciones de Cadena — LIKE, starts/ends/contains
  9. Operaciones de Fecha — WhereDate, casting de fecha por dialecto
  10. 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

← Volver a la Guía


From

Establece la tabla fuente. Todas las consultas empiezan aquí.

TQuery.New.From('pedidos')
// → SELECT * FROM pedidos

Con alias:

TQuery.New.From('pedidos', 'p')
// → SELECT * FROM pedidos p

Select — Lista de Columnas

TQuery.New.From('usuarios')
  .Select(['id', 'nombre', 'email'])
// → SELECT id, nombre, email FROM usuarios

Columna única:

.Select('nombre')
// → SELECT nombre FROM usuarios

Con alias:

.SelectAs('nombre_completo', 'nombre')
// → SELECT nombre_completo AS nombre FROM usuarios

SELECT * (por defecto)

Cuando no hay llamada a Select, se emite SELECT *:

TQuery.New.From('productos')
// → SELECT * FROM productos

Distinct

TQuery.New.From('pedidos')
  .Select('cliente_id')
  .Distinct
// → SELECT DISTINCT cliente_id FROM pedidos

SelectRaw

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 ventas

Funciones 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 estado

ORDER 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 DESC

Dó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 FIRST

El 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 ASC

Lo 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 40

Take 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

← Volver a la Guía


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 NULL

Pasar 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 = false

La 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 filas

IS 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 negada

Para 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ática

Un 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

← Volver a la Guía


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.id

LEFT JOIN

.LeftJoin('direcciones', 'usuarios.id', 'direcciones.usuario_id')
// → LEFT JOIN direcciones ON usuarios.id = direcciones.usuario_id

RIGHT JOIN

.RightJoin('departamentos', 'empleados.dept_id', 'departamentos.id')
// → RIGHT JOIN departamentos ON empleados.dept_id = departamentos.id

CROSS JOIN

.CrossJoin('tallas')
// → CROSS JOIN tallas

FULL OUTER JOIN

.FullOuterJoin('b', 'a.id', 'b.a_id')
// → FULL OUTER JOIN b ON a.id = b.a_id

Join 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 = 1

Join 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

← Volver a la Guía


GROUP BY

TQuery.New.From('ventas')
  .SelectCount('*', 'qty')
  .GroupBy('producto_id')
// → SELECT COUNT(*) AS qty FROM ventas GROUP BY producto_id

Múltiples columnas — llama a GroupBy varias veces o pasa un array:

.GroupBy(['año', 'mes', 'categoria'])
// → GROUP BY año, mes, categoria

Expresió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) >= 1000

Having 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(*) > 50

Operaciones de Conjunto

Operaciones de Conjunto y Paginación

🌍 Idioma: English | Español

← Volver a la Guía


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_archivados

UNION 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_eliminados

Paginació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 50

Tamaño de página por defecto: 15:

.ForPage(2)   // → LIMIT 15 OFFSET 15

LIMIT / 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

← Volver a la Guía


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 = true

UPDATE

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 = true

Patró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'))
  .AsDelete

Subconsultas

Subconsultas y CTEs

🌍 Idioma: English | Español

← Volver a la Guía


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 pedidos

WITH (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 recientes

CTE 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 arbol

WITH 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

← Volver a la Guía


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

← Volver a la Guía


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 valores TDateTime de Delphi — el driver de base de datos gestiona la conversión de tipo desde el binding Variant.

⚠️ 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

← Volver a la Guía


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/OFFSET vs FETCH NEXT … ROWS ONLY vs ROWS m TO n
  • Emisión de la ordenación de NULL — NULLS FIRST/LAST nativo, 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" = $1

Usar 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 BY con OFFSET/FETCH. Si la consulta no lo tiene, TSqlServerCompiler añade ORDER BY (SELECT 0) —ORDER BY 1 en una consulta Distinct—, 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 OrderBy sin 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 de TSQLiteCompiler y emular como hace MySQL.
  • Firebird no usa índice en una columna ordenada con una colocación NULLS distinta de la suya por omisión.