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
- 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.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 NULLVer 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 DESCLIMIT 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]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 también genera IS NULL:
.Where('eliminado_en', Null) // → WHERE eliminado_en IS NULLAbreviatura Booleana
.WhereTrue('activo') // → WHERE activo = TRUE
.WhereFalse('bloqueado') // → WHERE bloqueado = FALSEBETWEEN
.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
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.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
.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.idMú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
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
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 = ?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'))
.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])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 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')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 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
LOWERfunciona 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 | 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 | 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 | 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.
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 = $1Ejemplo 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 valoresTDateTimede Delphi — el driver de base de datos gestiona la conversión de tipo desde el bindingVariant.
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 —
?,$1,:p1,@p1, etc. - Cast de fechas — expresiones específicas del dialecto para
WhereDate/WhereDatePart - Paginación —
LIMIT/OFFSETvsFETCH NEXT … ROWS ONLYvsROWNUM
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 BYcuando se usaOFFSET/FETCH. Añade.OrderBy(…)antes de llamar a.ForPage(…)conTSqlServerCompiler.