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;

var Q := TQuery.New          // IQuery — con conteo de referencias, sin Free
  .From('productos')
  .Where('activo', True)
  .OrderBy('nombre')
  .Limit(50);

var R := TAnsiSqlCompiler.Create.Compile(Q);
// R.SQL      → 'SELECT * FROM productos WHERE activo = ? ORDER BY nombre LIMIT 50'
// R.Bindings → [True]

TSQLResult siempre contiene:

Campo Tipo Descripción
SQL string El SQL compilado con marcadores ? (los compiladores de dialecto pueden usar $1, :p1, @p1)
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.Compiler;             // TAnsiSqlCompiler (por defecto)
uses Daf.SQLCute.Compiler.Postgres;    // TPostgresCompiler
uses Daf.SQLCute.Compiler.MySql;       // TMySqlCompiler
// … etc.

var R := TPostgresCompiler.Create.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 (o IS NOT NULL para variantes negadas).

.Where('eliminado_en', Null)      // → WHERE eliminado_en IS NULL
.WhereNot('eliminado_en', Null)   // → WHERE eliminado_en IS NOT NULL

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

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]

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 también genera IS NULL:

.Where('eliminado_en', Null)     // → WHERE eliminado_en IS NULL

Abreviatura Booleana

.WhereTrue('activo')    // → WHERE activo = TRUE
.WhereFalse('bloqueado') // → WHERE bloqueado = 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'])

WHERE EXISTS (subconsulta)

var Sub := TQuery.New.From('lineas_pedido').Where('pedido_id', '=', 'pedidos.id');

TQuery.New.From('pedidos')
  .WhereExists(Sub)
// → WHERE EXISTS (SELECT * FROM lineas_pedido WHERE pedido_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 = ?)

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 = ? AND (stock > ? OR pedido_pendiente_permitido = ?)

Grupo con conector OR:

.OrWhere(function(Q: IQuery): IQuery
  begin
    Result := Q.Where('rol', 'admin').OrWhere('rol', 'moderador');
  end)
// → OR (rol = ? OR rol = ?)

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')

⚠️ 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 = ?   (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 =.

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
        .Where('lineas_pedido.pedido_id', '=', 'pedidos.id')
        .OrWhere('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)

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.Where('li.pedido_id', '=', 'pedidos.id');
    end)
// → INNER JOIN (SELECT pedido_id, SUM(qty) AS qty_total
//               FROM lineas_pedido GROUP BY pedido_id) 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

Having filtra grupos tras la agregación. El argumento columna acepta cualquier expresión de agregado.

TQuery.New.From('pedidos')
  .GroupBy('cliente_id')
  .SelectCount('*', 'num_pedidos')
  .SelectSum('total', 'ingresos')
  .Having('count(*)', '>', 5)
  .Having('sum(total)', '>=', 1000)
// → SELECT COUNT(*) AS num_pedidos, SUM(total) AS ingresos
//   FROM pedidos
//   GROUP BY cliente_id
//   HAVING count(*) > ? AND sum(total) >= ?
//   Bindings: [5, 1000]

Expresión HAVING cruda:

.HavingRaw('AVG(tiempo_respuesta) < 200')

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')
  .Having('count(*)', '>', 50)     // aplicado después de GROUP BY
// → SELECT COUNT(*) AS errores FROM logs
//   WHERE nivel = ?
//   GROUP BY servicio
//   HAVING count(*) > ?

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

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

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')
      .Where('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])

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')
      .Where('pedido_id', '=', 'pedidos.id')
      .Where('estado', 'completo'))
// → WHERE EXISTS (SELECT * FROM pagos
//                WHERE pedido_id = pedidos.id AND estado = ?)

Subconsulta Escalar en SELECT

var NumLineas := TQuery.New.From('lineas_pedido')
  .SelectCount('*')
  .Where('pedido_id', '=', 'pedidos.id');

TQuery.New.From('pedidos')
  .Select(NumLineas, 'num_lineas')
// → SELECT (SELECT 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')

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 tanto la columna como el valor en LOWER(…) para conseguir coincidencia insensible a mayúsculas. Pasa True como último argumento para omitir el envoltorio LOWER.


WhereLike — Patrón Libre

Proporciona el patrón LIKE completo:

.WhereLike('nombre', '%juan%')
// Insensible a mayúsculas (por defecto):
// → WHERE LOWER(nombre) LIKE lower('%juan%')

.WhereLike('codigo', 'ABC-%', True)
// Sensible a mayúsculas:
// → WHERE codigo LIKE 'ABC-%'

Variantes: WhereNotLike, OrWhereLike, OrWhereNotLike.


WhereStarts — Coincidencia de Prefijo

.WhereStarts('email', 'admin')
// → WHERE LOWER(email) LIKE lower('admin%')

Variantes: WhereNotStarts, OrWhereStarts, OrWhereNotStarts.


WhereEnds — Coincidencia de Sufijo

.WhereEnds('archivo', '.pdf')
// → WHERE LOWER(archivo) LIKE lower('%.pdf')

Variantes: WhereNotEnds, OrWhereEnds, OrWhereNotEnds.


WhereContains — Coincidencia de Subcadena

.WhereContains('descripcion', 'urgente')
// → WHERE LOWER(descripcion) LIKE lower('%urgente%')

Variantes: WhereNotContains, OrWhereContains, OrWhereNotContains.


Combinando Filtros de Cadena

TQuery.New.From('productos')
  .WhereContains('nombre', 'cafe')
  .OrWhereContains('etiquetas', 'cafe')
  .WhereNotStarts('sku', 'DESC-')
// → WHERE (LOWER(nombre) LIKE lower('%cafe%')
//          OR LOWER(etiquetas) LIKE lower('%cafe%'))
//     AND NOT (LOWER(sku) LIKE lower('DESC-%'))

Notas sobre Sensibilidad a Mayúsculas por Dialecto

El enfoque de envolver con LOWER funciona de forma fiable en todos los dialectos SQL, pero cada base de datos tiene su propio comportamiento nativo de LIKE:

Dialecto ¿LIKE sensible a mayúsculas? Notas
ANSI SQL Comportamiento estándar
PostgreSQL Admite ILIKE nativamente; SQLCute usa LOWER(…)
MySQL No (por defecto) El envoltorio LOWER no tiene efecto negativo
SQLite No para ASCII Los caracteres Unicode son sensibles sin la extensión ICU
SQL Server Depende de la intercalación Latin1_General_CI_AS → insensible a mayúsculas
Oracle Se recomienda el envoltorio LOWER
Firebird 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.


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.Compiler.Postgres;

var R := TPostgresCompiler.Create.Compile(
  TQuery.New.From('t').WhereDate('d', '2024-01-01'));
// → WHERE "d"::date = $1

Ejemplo MySQL

uses Daf.SQLCute.Compiler.MySql;

var R := TMySqlCompiler.Create.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.


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?, $1, :p1, @p1, etc.
  • Cast de fechas — expresiones específicas del dialecto para WhereDate/WhereDatePart
  • PaginaciónLIMIT/OFFSET vs FETCH NEXT … ROWS ONLY vs ROWNUM

Matriz de Dialectos

Clase del compilador Comillas Marcador 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 [] @p1, @p2 Daf.SQLCute.Compiler.SqlServer
TOracleCompiler " :p1, :p2 Daf.SQLCute.Compiler.Oracle
TFirebirdCompiler " ? Daf.SQLCute.Compiler.Firebird

Todos los compiladores de dialecto están en el paquete SQLCute.Compilers — añádelo a tu proyecto junto con SQLCute.Abstractions y SQLCute.


Usar un Compilador de Dialecto

uses
  Daf.SQLCute,
  Daf.SQLCute.Compiler.Postgres;

var Q := TQuery.New
  .From('usuarios')
  .Where('activo', True)
  .OrderBy('nombre');

var Compilador := TPostgresCompiler.Create;
var R := Compilador.Compile(Q);
// R.SQL      → 'SELECT * FROM "usuarios" WHERE "activo" = $1 ORDER BY "nombre"'
// R.Bindings → [True]

O usa el atajo Q.Compile(Compilador):

var R := Q.Compile(TPostgresCompiler.Create);

Inyección de Dependencias

Registra el compilador 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;

Implementar un Compilador Personalizado

Extiende TAnsiSqlCompiler y sobreescribe solo lo que difiere:

uses Daf.SQLCute.Compiler;

type
  TMiDbCompilador = class(TAnsiSqlCompiler)
  protected
    function WrapColumn(const Col: string): string; override;
    function WrapTable(const Table: string): string; override;
    function ParamPlaceholder: string; override;
  private
    FIndice: Integer;
  public
    function Compile(const Query: IQuery): TSQLResult; override;
  end;

function TMiDbCompilador.WrapColumn(const Col: string): string;
begin
  Result := '[' + Col + ']';
end;

function TMiDbCompilador.ParamPlaceholder: string;
begin
  Inc(FIndice);
  Result := ':param' + IntToStr(FIndice);
end;

function TMiDbCompilador.Compile(const Query: IQuery): TSQLResult;
begin
  FIndice := 0;
  Result := inherited Compile(Query);
end;

Paginación por Dialecto

SQLCute emite LIMIT … OFFSET … por defecto. Los compiladores de SQL Server y Oracle sobreescriben la paginación para usar su sintaxis nativa:

Dialecto SQL de paginación
ANSI / Postgres / MySQL / SQLite / Firebird 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

SQL Server requiere una cláusula ORDER BY cuando se usa OFFSET/FETCH. Añade .OrderBy(…) antes de llamar a .ForPage(…) con TSqlServerCompiler.