Trois des drivers de QoreDB (PostgreSQL, MySQL/MariaDB, SQLite) s'appuient sur SQLx. Ce choix n'est pas cosmétique : SQLx apporte un pool async natif, un typage précis des colonnes et une intégration propre avec tokio. Mais son argument marketing le plus connu, la vérification compile-time des requêtes, n'est pas utilisé dans QoreDB. Cet article explique ce que SQLx apporte réellement dans un client de base de données généraliste, et pourquoi certaines de ses fonctionnalités phares n'ont pas de sens ici.
Pourquoi SQLx plutôt qu'un driver natif par moteur
Chaque moteur SQL a son propre écosystème Rust : rust-postgres pour PostgreSQL, mysql_async pour MySQL, rusqlite pour SQLite. SQLx couvre les trois avec une API unifiée sur des types comme Pool, Row et Column. Pour QoreDB, ça veut dire écrire une fois la logique de streaming, de pagination ou de conversion des valeurs, puis la spécialiser par moteur uniquement là où le protocole diffère vraiment.
Concrètement, le module pg_compat sert de base commune aux moteurs qui parlent le protocole PostgreSQL (PostgreSQL, CockroachDB), et les drivers concrets n'implémentent que les overrides spécifiques : vues matérialisées, opérations de maintenance, valeurs par défaut de la chaîne de connexion. Sans SQLx, il faudrait dupliquer le code du pool, du dispatch de types et de la gestion des transactions pour chaque driver.
Le pool async, pièce centrale de chaque session
Une session QoreDB correspond à une connexion utilisateur ouverte depuis l'interface. Techniquement, chaque session détient un PgPool, un MySqlPool ou un SqlitePool configuré avec un nombre maximal de connexions, un timeout d'acquisition et un test query optionnel. Le pool est stocké dans une SessionMap partagée derrière un RwLock, ce qui permet à plusieurs requêtes lancées depuis des onglets différents de piocher des connexions en parallèle sans se bloquer.
Le pool async change la donne pour un client desktop. Une requête longue qui streame 100 000 lignes ne monopolise pas les autres opérations : l'inspection du schéma, l'autocomplétion, un COUNT sur une autre table, tout continue à répondre. Les connexions restent maintenues chaudes entre deux requêtes, ce qui évite le coût d'un handshake à chaque exécution.
Les transactions ont leur propre traitement. Quand une session ouvre un BEGIN, une connexion dédiée est extraite du pool via acquire() et stockée dans le champ transaction_conn de la session. Toutes les requêtes qui suivent transitent par cette connexion tant que le COMMIT ou ROLLBACK n'est pas exécuté. Sans ce mécanisme, SQLx renverrait chaque requête sur une connexion différente du pool et la transaction serait perdue.
Pourquoi je n'utilise pas les macros compile-time
SQLx est souvent présenté avec ses macros query! et query_as!, qui interrogent une base de données à la compilation pour vérifier que le SQL est valide et que les types des colonnes correspondent au struct Rust cible. C'est utile pour une application qui embarque son propre schéma. Ça n'a aucun sens pour un client généraliste.
Dans QoreDB, l'immense majorité des requêtes sont fournies par l'utilisateur au runtime : une commande SQL tapée dans l'éditeur, un filtre sur le Data Grid, un ORDER BY choisi dans un menu. Il n'y a pas de schéma connu à la compilation. Même les requêtes internes (introspection du schéma, count paginé, liste des collections) sont dynamiques par nature : elles varient selon le moteur, la version, le namespace ciblé. Utiliser les macros forcerait à figer une base de développement en dur, ce qui compromettrait la portabilité de la build.
L'API runtime de SQLx (sqlx::query, sqlx::query_as) apporte tout ce dont j'ai besoin : le bind sécurisé des paramètres via .bind(), la préparation côté serveur, la décoration des rows en type Rust natif. La sécurité vient du binding, pas de la vérification statique.
Le typage des colonnes, un vrai gain
Un client de BDD doit convertir n'importe quel type SQL en une valeur affichable. SQLx expose sur chaque Row la liste des colonnes avec leur nom et leur TypeInfo, ce qui permet de dispatcher vers le bon décodeur sans deviner. Pour PostgreSQL, ce dispatch construit une table de décodeurs typés au moment où le premier row arrive, en tenant compte des enums utilisateurs récupérés depuis pg_type. Résultat : les timestamps restent des timestamps, les numeric ne perdent pas de précision, les enums sont lus comme du texte plutôt que comme des OID.
SQLite fait exception. Le moteur étant dynamiquement typé, un même column peut contenir un INTEGER, un TEXT ou un BLOB selon la ligne. Le driver QoreDB inspecte la classe de stockage réelle de chaque valeur via try_get_raw au lieu de faire confiance à la déclaration de colonne. C'est plus verbeux, mais c'est le seul moyen de restituer fidèlement ce que la base contient réellement.
Ce que les configurations after_connect apportent
SQLx permet d'exécuter des instructions sur chaque nouvelle connexion du pool via .after_connect(). C'est le point d'entrée pour normaliser le comportement d'un serveur, quel que soit son sql_mode ou son time_zone global. Le driver MySQL force NO_BACKSLASH_ESCAPES, utf8mb4 et le fuseau UTC à l'ouverture de chaque connexion. Sans ça, la fonction de safety qui échappe les quotes doublées serait défaite par un serveur configuré autrement, et les TIMESTAMP dériveraient selon le time zone du serveur.
Pour SQLite, la configuration se fait au niveau des SqliteConnectOptions : mode WAL activé pour permettre lecteurs et écrivains concurrents, busy_timeout à 30 secondes pour éviter les erreurs SQLITE_BUSY quand une autre application accède au fichier, création automatique du fichier si absent. Ces options sont des choix explicites : ils traduisent la façon dont on veut voir SQLite fonctionner dans un contexte desktop, où plusieurs processus peuvent viser la même base.
Le streaming des résultats
SQLx expose sur chaque pool une méthode fetch() qui renvoie un Stream de rows. Combiné avec futures::StreamExt, ça permet d'itérer une ligne à la fois côté application, sans charger tout le résultat en mémoire. QoreDB s'appuie dessus pour ses exports longs et son mode streaming du Data Grid : la mémoire consommée reste bornée quel que soit le volume de la table. Le pool récupère la connexion à la fin du stream, transparent pour l'appelant.
Les moteurs qui n'utilisent pas SQLx
SQLx n'est pas la solution à tout. MongoDB s'appuie sur le driver officiel mongodb-rust, plus proche de BSON et des aggregation pipelines. Redis passe par la crate redis avec connection-manager pour la reconnexion transparente. SQL Server utilise Tiberius derrière un pool bb8, parce que SQLx ne supporte pas TDS. DuckDB utilise sa propre crate bundled, qui parle directement au moteur embarqué. Chaque driver choisit la couche la plus adaptée à son protocole, sans tenter de forcer une abstraction uniforme.
En pratique, à l'usage
Le résultat est un backend qui reste léger et prédictible. Ouvrir une connexion PostgreSQL déclenche la création d'un pool avec un test query SELECT 1, puis toutes les opérations de l'interface (lister les schemas, décrire une table, exécuter une query, streamer un export) partagent le même pool sans recréer de socket. Fermer la connexion depuis l'interface libère explicitement le pool. Les fuites de connexion, qui sont un fléau classique des clients desktop, deviennent structurellement impossibles.
Conclusion
SQLx apporte à QoreDB un pool async solide, une abstraction cohérente entre PostgreSQL, MySQL et SQLite, et un typage précis des colonnes. Ses macros compile-time sont un bel outil, mais elles ciblent un cas d'usage différent du nôtre : elles brillent quand le schéma est connu à la build, pas quand l'utilisateur tape une requête ad hoc. En s'en tenant à l'API runtime, QoreDB garde tous les bénéfices de SQLx tout en restant un client généraliste qui parle à n'importe quelle base à l'exécution.
Restez informé des nouveautés
Rejoignez notre newsletter pour recevoir les mises à jour majeures, les nouveaux drivers et nos coulisses techniques.

