AskPostgreSQL : Posez une question à votre base de données, en anglais
| Stack | Next.js 15, PostgreSQL, GPT-5 Mini, Recharts, NextAuth.js |
| Focus | Langage naturel vers SQL, pipelines LLM, visualisation de données |
Ce que ça fait
AskPostgreSQL transforme une question posée en anglais courant en une requête SQL fonctionnelle, l'exécute, explique ce qu'elle a fait, et dessine le bon graphique, le tout en une seule requête. Demandez « quelle industrie a la valorisation moyenne la plus élevée en Europe » et obtenez en retour un graphique en barres, le SQL exact derrière, et une explication en langage simple de ce que fait chaque clause.
Le problème des outils « chat avec vos données »
La plupart des gens qui veulent une réponse d'une base de données ne veulent pas apprendre le SQL pour l'obtenir. La plupart des outils « chat avec vos données » cachent entièrement la requête, ce qui rend la réponse invérifiable, ou déversent du SQL brut sur quelqu'un qui ne sait pas le lire, ce qui rend l'outil inutile pour la personne qui en avait besoin au départ.
AskPostgreSQL montre les deux : la réponse, et la requête exacte qui l'a produite, expliquée dans l'ordre.
Quatre appels, de la question au graphique
Quatre étapes portent une question du texte au graphique, trois d'entre elles étant des appels à GPT-5 Mini, une étant une vérification locale qui n'en est délibérément pas un :
- 01
generateQuery
Transforme la question en une seule instruction SELECT, avec le vrai schéma de la table dans le prompt système. Il connaît les particularités spécifiques de ce jeu de données : les valorisations sont stockées en milliards, les noms de pays comme « UK » sont développés en « United Kingdom » avant d'atteindre la clause WHERE, et les noms d'industries doivent correspondre à une liste fixe qui a son propre désordre intégré, « healthcare & life sciences » et « health » sont deux catégories valides parce que les données sous-jacentes ont les deux, donc le prompt contourne le problème plutôt que de le nettoyer.
- 02
Le garde-fou
La requête générée passe par une vérification qui n'autorise que les instructions SELECT. Tout ce qui contient DROP, DELETE, INSERT, UPDATE, ALTER, TRUNCATE, CREATE, GRANT, ou REVOKE est rejeté avant d'atteindre Postgres.
- 03
explainQuery
Découpe le même SQL en sections étiquetées, SELECT, FROM, WHERE, GROUP BY, et écrit une ligne en langage simple pour chacune, en ne présumant aucune connaissance du SQL.
- 04
generateChartConfig
Examine la forme réelle de l'ensemble de résultats et choisit un type de graphique, des clés d'axes, et des couleurs par série, en passant à une comparaison multi-lignes dès qu'il voit plus d'une catégorie à tracer sur le même axe.
Le repli de parsing, et son coût visible
Chacun des trois appels au modèle demande du JSON structuré en retour, et le parser ne fait pas simplement confiance à ce qu'il a reçu. Il essaie d'abord un parsing JSON direct, puis un bloc de code markdown, puis une correspondance regex sur un littéral d'objet JS brut, puis une extraction regex pour une instruction SQL brute, puis un parsing en liste à puces pour les explications, et alors seulement se replie sur le traitement de toute la réponse comme du texte brut. Cinq stratégies de repli pour un seul appel au modèle.
Le texte de description et de conclusion du graphique sous l'image principale ci-dessus, c'est à quoi ce repli ressemble en pratique : quand le JSON du modèle omet un champ optionnel, l'application le remplit avec une valeur par défaut codée en dur plutôt que de faire échouer la requête. C'est le chemin de dégradation en douceur qui fonctionne comme prévu, et de l'extérieur, un repli qui fonctionne et un texte de substitution visible se ressemblent exactement.
Le garde-fou SQL, et ses limites
Le garde-fou SQL ici est une liste noire de mots-clés, pas un générateur de requêtes paramétrées. C'est un compromis délibéré pour une démo tournant sur un jeu de données synthétique préchargé, sans aucun chemin d'écriture exposé ailleurs : il bloque les catégories de requêtes qui comptent, les mutations et les changements de schéma, sans avoir besoin d'un générateur de requêtes complet devant un modèle qui doit déjà, par conception, écrire des instructions SELECT arbitraires.
La liste noire n'est pas non plus consciente des tokens, c'est une simple vérification de sous-chaîne sur le texte de la requête en minuscules. Demandez les entreprises avec « updated » dans le nom et la requête est rejetée avant d'atteindre Postgres, non pas parce que le SQL fait quoi que ce soit de dangereux, mais parce que le mot « update » apparaît à l'intérieur d'un autre mot. Une vraie faille, et sur ce jeu de données une rare : aucune licorne ne porte un nom qui s'en approche.
Ce ne serait pas le bon choix devant une base de données de production contenant de vraies données utilisateur, où la surface de requête et la frontière de confiance ont toutes deux une allure complètement différente.
Où vont réellement les données
Deux des trois appels au modèle ne voient jamais que le schéma et votre question. generateChartConfig voit plus : les résultats réels de la requête, sérialisés en JSON, envoyés au modèle pour qu'il puisse déduire la bonne forme de graphique à partir des données réelles plutôt que de la seule question. Sur un jeu de données préchargé de valorisations publiques de licornes, ce n'est pas un problème.
Devant une base de données de production, c'est le même problème de frontière de confiance que le garde-fou SQL, un appel plus profond. Le garde-fou SQL contrôle ce qui sort du modèle. Rien ici ne contrôle ce qui y entre.
Choisir le bon graphique
Côté frontend, un composant de graphique dynamique lit cette configuration et l'affiche directement dans Recharts : barres, lignes, camembert, ou comparaison multi-lignes selon ce que le modèle a choisi. Les couleurs reviennent sous forme de références à des variables CSS plutôt que de valeurs hexadécimales codées en dur, si bien que chaque graphique reste cohérent avec le thème, en mode clair comme en mode sombre, sans que le modèle ait jamais besoin de connaître la palette de l'application.
Requêtes sauvegardées
Chaque requête exécutée par un utilisateur connecté est sauvegardée avec son SQL, ses résultats, et sa configuration de graphique attachée, si bien que rouvrir une ancienne question ne veut pas dire la reposer depuis zéro. Un panneau de requêtes suggérées donne aux nouveaux utilisateurs un point de départ avec des questions pré-écrites sur le jeu de données préchargé, 1 277 vraies entreprises licornes, plutôt qu'une zone de texte vide et un curseur qui clignote.
Ce que je referais
generateChartConfig est l'appel que je referais en premier. Confier à un modèle l'ensemble de résultats brut et lui demander d'inventer des clés d'axes et des couleurs à partir de rien fonctionne, mais c'est l'étape la moins déterministe du pipeline, et celle la plus susceptible de choisir un graphique bizarre pour une question qui avait une réponse évidente.
Une petite couche à base de règles devant lui, n'escaladant vers le modèle que pour les formes de résultats ambiguës, serait plus fiable qu'un appel LLM à chaque requête. Cela réduirait aussi la fréquence à laquelle l'ensemble complet de résultats doit ne serait-ce que quitter le serveur, tout ce qui a une forme évidente est classé localement, et seuls les cas véritablement ambigus envoient une copie des données à OpenAI.