Un client de base de données expose à un seul clic des opérations qui peuvent vider une table, supprimer un schéma ou désactiver des garanties d'intégrité. Quand l'éditeur SQL accepte tout texte saisi, la responsabilité de filtrer ce qui peut tourner sans confirmation revient au client lui-même.
Dans QoreDB, cette classification ne dépend pas d'une recherche de mots-clés dans la barre d'édition. Elle s'appuie sur un parseur SQL complet, exécuté à chaque soumission, et sur l'environnement déclaré pour la connexion. Cet article décrit comment fonctionne ce mécanisme et pourquoi il a été conçu de cette façon.
Pourquoi un parseur SQL plutôt qu'une regex
Détecter une requête destructrice avec une regex est tentant car simple à écrire. Une recherche de DROP, TRUNCATE ou DELETE semble couvrir l'essentiel. En pratique, cette approche a deux défauts.
Le premier est qu'elle produit des faux positifs. Une colonne nommée delete_at, un commentaire qui contient DROP TABLE, un alias ou un littéral suffisent à déclencher l'alarme. Le second est plus grave: la regex ignore le contexte syntaxique. Elle ne peut pas distinguer UPDATE users SET name = 'x' (mise à jour de toutes les lignes) de UPDATE users SET name = 'x' WHERE id = 1 (mise à jour d'une seule ligne) sans risquer d'autres faux positifs.
QoreDB utilise donc sqlparser-rs, un parseur Rust qui retourne un AST typé. La classification se fait en visitant l'arbre, pas en cherchant des chaînes.
- Un
Statement::Drop,Statement::TruncateouStatement::AlterTableest marqué dangereux. - Un
Statement::UpdateouStatement::Deleten'est marqué dangereux que si son champselection(la clause WHERE) estNone. - Une
SELECT ... INTOest détectée comme mutation grâce au champselect.into.is_some().
Cette analyse s'exécute pour chaque requête, sans dépendre d'un mode particulier. Le coût d'un parsing complet n'est pas négligeable pour les requêtes longues, donc une cache LRU de 256 entrées par paire (driver, sql) évite de re-parser une requête identique relancée plusieurs fois dans la session.
Une analyse côté backend, pas seulement un avertissement d'UI
L'UI affiche les bannières et confirmations associées, mais la vérification ne peut pas vivre uniquement dans le frontend. La même commande IPC est appelée par plusieurs entrées: l'éditeur SQL, l'inline edit du data grid, l'export, certains hooks de plugin. Concentrer la logique au niveau de la commande Rust garantit qu'aucun chemin d'appel ne peut court-circuiter la classification.
Le résultat de l'analyse vit dans la struct SqlSafetyAnalysis { is_mutation, is_dangerous }. Cette struct est consommée par le query pipeline avant l'exécution. C'est à ce moment que la décision est prise: passer à l'exécution, demander une confirmation, ou refuser.
Le rôle de l'environnement de connexion
Une requête DROP TABLE n'a pas le même poids selon la base ciblée. Sur une instance locale jetable, c'est anodin. Sur une réplique de production, c'est un incident. QoreDB attache à chaque connexion un environnement déclaré: Dev, Staging ou Prod. La classification produite par le parseur est ensuite croisée avec cet environnement et la policy active.
Concrètement, en environnement Prod et avec une requête marquée dangereuse, deux comportements sont possibles, selon la policy.
- Si
prod_block_dangerous_sqlest actif, la requête est refusée sans appel. - Sinon, si
prod_require_confirmationest actif et qu'aucune acceptation explicite n'a été transmise, la requête est refusée avec une demande de confirmation.
Cette acceptation explicite n'est pas un simple booléen passé depuis le frontend. Pour les actions IPC les plus sensibles, QoreDB exige un jeton de confirmation: l'UI le demande à un endpoint dédié, le backend le délivre avec un TTL de 60 secondes, lié à un nom d'action précis, et il est consommé en une seule fois. Un appel direct au binding Tauri ne peut donc pas falsifier l'acceptation, il devra suivre exactement le même cycle.
Au-delà des classiques : les dangers spécifiques par moteur
DROP et TRUNCATE ne suffisent pas à couvrir tous les cas où une requête peut élargir la surface d'attaque ou casser la base. Deux moteurs ont droit à des classifieurs supplémentaires.
DuckDB expose des commandes qui peuvent faire sortir des données ou faire entrer du code, et qui n'apparaissent pas dans le périmètre classique des requêtes destructrices.
INSTALL <extension>télécharge une extension depuis un repository.LOAD <extension>active potentiellement de l'I/O réseau ou disque dans la session.ATTACH '...'monte une base arbitraire, y compris en HTTP.COPY ... TO '<path>'écrit vers un chemin filesystem que l'utilisateur n'a pas forcément voulu autoriser.PRAGMA enable_external_accessrouvre l'accès externe au sein du processus.
SQLite, de son côté, a des PRAGMAs qui modifient les garanties de la base sans toucher aux données :
ATTACH DATABASE '...'monte un fichier arbitraire.PRAGMA writable_schema = 1permet d'éditer directementsqlite_master.PRAGMA journal_mode = OFFdésactive la durabilité.PRAGMA foreign_keys = OFFdésactive l'intégrité référentielle.
Ces classifieurs reposent sur l'inspection du mot-clé en tête de requête, après nettoyage des commentaires et des espaces. Ils sont conservatifs: seules les formes explicitement dangereuses sont marquées. Un PRAGMA table_info(users) ou un COPY t FROM 'file.csv' passent sans encombre, parce qu'ils n'ouvrent pas de capacité supplémentaire au-delà de ce que la session a déjà.
ClickHouse a un traitement à part. Son dialecte (ENGINE, ARRAY JOIN, FORMAT, SETTINGS) n'est pas entièrement couvert par sqlparser-rs, et un parsing strict produirait des erreurs sur du SQL valide. QoreDB bascule donc sur un classifieur par mots-clés pour ClickHouse, conservatif lui aussi: tout segment non reconnu est traité comme une mutation, ce qui garde le garde-fou actif en lecture seule.
Le résultat côté utilisateur
Le compromis pratique de cette architecture est lisible dans l'expérience quotidienne. En environnement Dev, le mécanisme est silencieux. Une requête DELETE FROM users part directement, comme attendu. En Staging, les marqueurs is_mutation et is_dangerous alimentent l'interceptor et les hooks de plugin sans bloquer l'exécution. En Prod, la classification déclenche soit un refus immédiat soit une demande de confirmation, selon la policy choisie.
Le coût pour le développeur est minime: deux passes de parsing pour une requête nouvelle, une lecture de cache LRU pour une requête répétée. La structure de la classification (is_mutation et is_dangerous) reste le seul point d'entrée pour toutes les décisions en aval: read-only mode, confirmation prod, interceptor pipeline, hooks. C'est un seul endroit à raisonner et un seul endroit à tester.
Conclusion
La détection des requêtes destructrices dans QoreDB repose sur trois principes assumés: un parseur d'AST plutôt qu'une regex, une analyse côté backend plutôt que dans l'UI, et une décision pondérée par l'environnement déclaré pour la connexion. Le résultat est un mécanisme discret en développement, fiable en production, auditable depuis un seul fichier Rust. C'est précisément le compromis que recherche un client desktop pour un développeur ou une petite équipe.
Restez informé des nouveautés
Rejoignez notre newsletter pour recevoir les mises à jour majeures, les nouveaux drivers et nos coulisses techniques.

