Brilliancy of quality
Documentación

Sintaxis práctica de SQLMacros para gestionar consultas de base de datos

Sapphire I.C.D.S.

Finalidad y límites de SQLMacros

SQLMacros es la capa de plantillas para SQL en Sapphire I.C.D.S. Una consulta normal se guarda en el directorio SQL/<Module>/<dialect>/, recibe una tabla de valores preparada, genera el texto SQL final y lo entrega al controlador activo de la base de datos. SQLMacros no abre una conexión, no elige el backend, no inicia una transacción ni decide qué consulta está permitido ejecutar. Estas responsabilidades permanecen en el módulo y en la capa de DB.

Mantenga archivos separados para MySQL y PostgreSQL aunque las consultas sean parecidas. Las comillas de los identificadores, las funciones de fecha, las conversiones de tipo, los upsert y los agregados difieren. Intentar ocultar dos dialectos en una plantilla compleja suele ser peor que disponer de dos archivos breves y verificables.

Sustitución básica con Table.Data

Los siguientes ejemplos utilizan un esquema ilustrativo con las tablas example_articles y example_article_notes; no son tablas reales de Sapphire I.C.D.S.

La principal fuente de datos de entrada es Table.Data("field"). Para una sola fila, el lado invocador normalmente ya ha preparado el cursor. Ejemplo de selección con parámetros numéricos:

SELECT id, topic, created_at
FROM example_articles
WHERE layout_id = {{ Table.Data("layout_id") }}
  AND language_id = {{ Table.Data("language_id") }}
ORDER BY id DESC
LIMIT {{ Table.Data("limit") }} OFFSET {{ Table.Data("offset") }};

De esta forma solo pueden mostrarse valores que el módulo haya creado como enteros y comprobado por rango. Una cadena de usuario como "10; DELETE ..." no debe llegar a un campo numérico. El macro no convierte un texto arbitrario en un número seguro.

Literales de cadena y escape

Una cadena se coloca entre comillas SQL y el valor se procesa mediante el DB escape provider activo:

SELECT id, topic
FROM example_articles
WHERE status = '{{ escape(Table.Data("status")) }}'
  AND LOWER(topic) LIKE CONCAT('%', LOWER('{{ escape(Table.Data("query")) }}'), '%');

escape(...) y la variante compatible Escape(...) escapan el contenido para el controlador seleccionado. Las comillas que rodean el literal de cadena siguen formando parte de la plantilla SQL. No utilice HTML escaping, sustitución manual de apóstrofos ni URL encoding en lugar del escape de DB.

Escapar un valor no permite convertirlo en identificador. Nunca escriba ORDER BY {{ Table.Data("sort") }}, FROM {{ ... }} ni un nombre de columna dinámico tomado de una solicitud del usuario. El módulo selecciona las opciones permitidas de ordenación y columnas mediante una allowlist; después invoca una plantilla estática concreta o transmite un fragmento seguro conocido de antemano conforme a un contrato separado.

Condiciones en la plantilla

Las construcciones de control se escriben entre {% y %}. Son útiles para un número reducido de filtros opcionales:

SELECT id, topic, status
FROM example_articles
WHERE layout_id = {{ Table.Data("layout_id") }}
{% if Table.Data("status") %}
  AND status = '{{ escape(Table.Data("status")) }}'
{% endif %}
{% if Table.Data("query") %}
  AND LOWER(topic) LIKE CONCAT('%', LOWER('{{ escape(Table.Data("query")) }}'), '%')
{% endif %}
ORDER BY id DESC
LIMIT {{ Table.Data("limit") }};

Asegúrese de que cualquier combinación de condiciones produzca una consulta sintácticamente correcta. Resulta práctico comenzar con un WHERE obligatorio y añadir las partes opcionales mediante AND. Si hay muchas ramas, es preferible crear varias consultas lógicas: son más fáciles de probar y mantener de forma segura.

Un comentario de la plantilla tiene la forma {# explicación #} y no aparece en el SQL final. Un -- SQL comment normal permanece en la consulta. No guarde en comentarios secretos, datos personales reales ni soluciones provisionales para eludir permisos.

INSERT y UPDATE

INSERT INTO example_article_notes (article_id, note, created_at)
VALUES ({{ Table.Data("article_id") }},
        '{{ escape(Table.Data("note")) }}',
        NOW());

Aquí, article_id debe ser un número positivo procedente de una tabla estructurada de confianza, mientras que note debe ser una cadena procesada con escape. La comprobación del propietario, los derechos del usuario, la existencia del artículo y la longitud permitida de la nota se realiza antes de la consulta. SQLMacros no añade automáticamente esta política.

En update/delete, incluya siempre una restricción precisa de entidad y, cuando corresponda, de propietario o layout. Una modificación masiva sin WHERE no resulta aceptable por el mero hecho de estar guardada en una plantilla. Para la edición concurrente, use el revision/transaction contract adoptado por el módulo, no una cadena improvisada.

Pager: consulta de datos y count

Una lista paginada suele utilizar dos plantillas: la consulta principal con LIMIT/OFFSET y un archivo *.count.sql. Sus condiciones de filtrado deben coincidir; de lo contrario, Pager mostrará un número de páginas incorrecto. Un orden de comprobación práctico es:

  1. copiar únicamente el bloque de filtros en la consulta count;
  2. probar una búsqueda vacía;
  3. probar una cadena con un apóstrofo y caracteres de porcentaje;
  4. probar limit 1, la última página y un offset fuera del resultado;
  5. comparar el número de filas de la consulta principal con count usando la misma entrada.

Environment, POST y Cookie

SQLMacros proporciona técnicamente valores del entorno y de la solicitud mediante llamadas como Env.Get(...), Post.Get(...) y Cookie.Get(...). No acceda a ellos directamente en la lógica SQL habitual de la aplicación. El módulo debe leer la entrada HTTP, comprobar su tipo, normalizar el valor, aplicar los permisos y colocar el resultado en Table. Este recorrido hace que el SQL sea independiente del transporte y apto para CGI, FastCGI, AJAX, un servicio o una prueba.

Qué se considera una plantilla segura

  • La estructura SQL, las tablas, las columnas y los operadores están escritos de forma estática.
  • Cada cadena pasa por escape(Table.Data(...)) dentro de un literal de cadena.
  • Los números, indicadores, limit y offset se tipan y limitan antes del renderizado.
  • La plantilla se encuentra en el módulo y en el directorio de dialecto correctos.
  • La selección de la consulta y el permiso para ejecutarla permanecen en el módulo.
  • Las pruebas y el diagnóstico confirman que un resultado satisfactorio no contiene {{ ... }} o {% ... %} sin resolver.
  • En caso de fallo de SQLMacros, el runtime actual puede devolver el texto original de la plantilla; el entorno DB invocador no garantiza un bloqueo fail-closed separado. No trate el renderizado como una security boundary: estos errores deben detectarse mediante pruebas y registros antes de production.

Pruebe los SELECT con datos de prueba y las consultas de modificación dentro de una transacción controlada o en una base aislada. No pruebe un DELETE/UPDATE nuevo en production. SQLMacros reduce la duplicación y centraliza el escape, pero la seguridad solo existe junto con una entrada tipada, autorización del servidor y una estructura de consulta estática.

Diagnóstico del resultado renderizado

Cuando se produzca un error, registre primero el nombre lógico de la consulta, el dialecto y el conjunto de campos de entrada sin valores secretos. Obtenga después el SQL final en un entorno de diagnóstico seguro y compruebe que se hayan resuelto todos los macros, que las comillas de cadena estén equilibradas y que las ramas opcionales hayan generado el texto previsto. No intente arreglar el problema añadiendo comillas arbitrarias alrededor de números: vuelva al tipo del campo y al contrato del módulo invocador.

Para una consulta nueva, prepare casos límite: cadena vacía, apóstrofo, Unicode, cero resultados, longitud máxima permitida e identificadores mínimo y máximo. La comprobación debe cubrir ambos archivos de dialecto compatibles. Aunque un backend no se utilice localmente, la consulta no debe considerarse terminada hasta superar una comprobación sintáctica o de integración en el entorno correspondiente.