Brilliancy of quality
Documentation

Syntaxe pratique de SQLMacros pour gérer les requêtes de base de données

Sapphire I.C.D.S.

Rôle et limites de SQLMacros

SQLMacros est la couche de modèles SQL de Sapphire I.C.D.S. Une requête ordinaire est conservée dans le répertoire SQL/<Module>/<dialect>/, reçoit une table de valeurs préparée, produit le texte SQL final et le transmet au pilote de base de données actif. SQLMacros n'ouvre pas de connexion, ne choisit pas le backend, ne commence pas de transaction et ne décide pas quelle requête est autorisée. Ces responsabilités demeurent dans le module et la couche DB.

Conservez des fichiers distincts pour MySQL et PostgreSQL, même si les requêtes se ressemblent. Les guillemets des identifiants, les fonctions de date, les conversions de types, les upsert et les agrégats diffèrent. Masquer deux dialectes dans un seul modèle complexe est généralement moins pertinent que deux fichiers courts et vérifiables.

Substitution de base avec Table.Data

Les exemples suivants utilisent un schéma illustratif contenant les tables example_articles et example_article_notes ; ce ne sont pas de vraies tables de Sapphire I.C.D.S.

La principale source de données d'entrée est Table.Data("field"). Pour une ligne unique, le côté appelant a généralement déjà préparé le curseur. Exemple de sélection avec des paramètres numériques :

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") }};

Cette méthode ne permet d'afficher que les valeurs que le module a créées comme entiers et contrôlées selon leur plage. Une chaîne utilisateur telle que "10; DELETE ..." ne doit pas atteindre un champ numérique. La macro ne transforme pas un texte arbitraire en nombre sûr.

Chaînes littérales et escape

Une chaîne est placée entre des guillemets SQL et sa valeur passe par le DB escape provider actif :

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

escape(...) et la variante compatible Escape(...) échappent le contenu pour le pilote choisi. Les guillemets entourant la chaîne littérale restent dans le modèle SQL. N'utilisez pas de HTML escaping, de remplacement manuel des apostrophes ou de URL encoding à la place de l'échappement DB.

Échapper une valeur ne permet pas de l'utiliser comme identifiant. N'écrivez jamais ORDER BY {{ Table.Data("sort") }}, FROM {{ ... }} ou un nom de colonne dynamique provenant d'une requête utilisateur. Le module choisit les options autorisées de tri et de colonnes dans une allowlist, puis appelle un modèle statique précis ou transmet un fragment sûr connu à l'avance selon un contrat distinct.

Conditions dans le modèle

Les constructions de contrôle sont écrites entre {% et %}. Elles conviennent à un petit nombre de filtres facultatifs :

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") }};

Veillez à ce que toute combinaison de conditions produise une requête syntaxiquement correcte. Il est pratique de commencer par un WHERE obligatoire, puis d'ajouter les parties facultatives avec AND. S'il existe de nombreuses branches, créez plutôt plusieurs requêtes logiques : elles seront plus faciles à tester et à maintenir en toute sécurité.

Un commentaire du moteur de modèles prend la forme {# explication #} et ne figure pas dans le SQL final. Un -- SQL comment ordinaire reste dans la requête. Ne conservez dans les commentaires ni secrets, ni données personnelles réelles, ni contournements temporaires de droits.

INSERT et UPDATE

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

Ici, article_id doit être un nombre positif provenant d'une table structurée de confiance, tandis que note doit être une chaîne traitée par escape. Le propriétaire, les droits de l'utilisateur, l'existence de l'article et la longueur autorisée de la note sont contrôlés avant la requête. SQLMacros n'ajoute pas automatiquement cette politique.

Pour update/delete, incluez toujours une restriction précise sur l'entité et, lorsque nécessaire, sur le propriétaire ou le layout. Une modification massive dépourvue de WHERE ne devient pas acceptable parce qu'elle est conservée dans un modèle. Pour les modifications concurrentes, employez le revision/transaction contract adopté par le module, et non une chaîne improvisée.

Pager : requête de données et count

Une liste paginée utilise généralement deux modèles : la requête principale avec LIMIT/OFFSET et un fichier *.count.sql. Leurs conditions de filtrage doivent correspondre, sinon Pager affichera un nombre de pages incorrect. Un ordre de vérification pratique consiste à :

  1. copier uniquement le bloc de filtres dans la requête count ;
  2. tester une recherche vide ;
  3. tester une chaîne contenant une apostrophe et des caractères de pourcentage ;
  4. tester limit 1, la dernière page et un offset au-delà des résultats ;
  5. comparer le nombre de lignes de la requête principale à count avec la même entrée.

Environment, POST et Cookie

SQLMacros fournit techniquement des valeurs d'environnement et de requête au moyen d'appels comme Env.Get(...), Post.Get(...) et Cookie.Get(...). N'y accédez pas directement pour la logique SQL applicative habituelle. Le module doit lire l'entrée HTTP, vérifier son type, normaliser la valeur, appliquer les droits et placer le résultat dans Table. Ce parcours rend le SQL indépendant du transport et utilisable avec CGI, FastCGI, AJAX, un service ou un test.

Critères d'un modèle sûr

  • La structure SQL, les tables, les colonnes et les opérateurs sont écrits statiquement.
  • Chaque chaîne passe par escape(Table.Data(...)) à l'intérieur d'une chaîne littérale.
  • Les nombres, indicateurs, limit et offset sont typés et limités avant le rendu.
  • Le modèle se trouve dans le bon module et le bon répertoire de dialecte.
  • Le choix de la requête et l'autorisation de l'exécuter restent dans le module.
  • Les tests et le diagnostic confirment qu'un résultat réussi ne contient aucun {{ ... }} ou {% ... %} non résolu.
  • En cas d'échec de SQLMacros, le runtime actuel peut renvoyer le texte d'origine du modèle ; le périmètre DB appelant ne garantit pas de blocage fail-closed distinct. Ne considérez pas le rendu comme une security boundary : ces erreurs doivent être détectées par les tests et les journaux avant production.

Testez les SELECT avec des données de test, et les requêtes de modification dans une transaction contrôlée ou une base isolée. Ne testez pas un nouveau DELETE/UPDATE en production. SQLMacros réduit la duplication et centralise l'échappement, mais la sécurité n'existe qu'avec une entrée typée, une autorisation côté serveur et une structure de requête statique.

Diagnostic du résultat du rendu

Lors d'une erreur, consignez d'abord le nom logique de la requête, le dialecte et l'ensemble des champs d'entrée sans valeurs secrètes. Obtenez ensuite le SQL final dans un environnement de diagnostic sûr et assurez-vous que toutes les macros ont été résolues, que les guillemets des chaînes sont équilibrés et que les branches facultatives ont produit le texte attendu. Ne tentez pas de corriger le problème en ajoutant des guillemets arbitraires autour des nombres : revenez au type du champ et au contrat du module appelant.

Pour une nouvelle requête, préparez des cas limites : chaîne vide, apostrophe, Unicode, aucun résultat, longueur maximale autorisée et identifiants minimal et maximal. La vérification doit couvrir les deux fichiers de dialectes pris en charge. Même si un backend n'est pas utilisé localement, la requête ne doit pas être considérée comme prête avant une vérification syntaxique ou d'intégration dans l'environnement correspondant.