Ce que le texte vers SQL doit savoir
Le texte vers SQL, c’est un modèle de langage qui transforme une question en mots simples en requête SQL. Le modèle n’a jamais vu votre base : il devine les tables que vous ne décrivez pas.
Le module 4 enseigne le SQL et le module 63 demande une forme fixe à un modèle. Cette leçon décide de ce qu’il faut dire au modèle sur vos tables.
À la fin de cette leçon
Remarque
Je peux décrire le schéma d’une base assez brièvement pour qu’un modèle en tire du SQL.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Texte vers SQL | un modèle qui change une question en requête | |
| Schéma | les tables et les colonnes d’une base | |
| Clé étrangère | une colonne qui désigne une ligne d’une autre table | prets.livre_id |
| Jointure | l’appariement des lignes de deux tables sur une colonne | |
| Note | une ligne qui dit ce que signifient les valeurs d’une colonne |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Le minimum : tables, colonnes et jointures
Pour écrire une requête, le modèle a besoin de trois choses. Il lui faut le nom des tables, les colonnes de chaque table et la façon dont les tables se joignent.
Sans les jointures, il devine. Joindre prets.id à livres.id, par exemple, tourne sans erreur et donne les mauvais livres.
Exemple détaillé : le schéma de la médiathèque
| Table | Colonnes | Se joint à |
|---|---|---|
| livres | id, titre, annee | Rien |
| membres | id, nom | Rien |
| prets | id, livre_id, membre_id, mois, rendu | livre_id à livres.id, membre_id à membres.id |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Certaines colonnes demandent une note
Le nom d’une colonne ne dit pas toujours ce que veulent dire ses valeurs. Dans la médiathèque, rendu vaut 1 quand le livre est revenu et 0 tant qu’il est dehors.
Un modèle qui ne lit que le nom pourrait tester rendu = « non » ou rendu IS NULL. Une note courte par colonne floue évite cette supposition.
Exemple détaillé : des noms qui demandent une note
| Colonne | Une supposition sans note | La note |
|---|---|---|
| prets.rendu | rendu = « non » | 1 si rendu, 0 si encore dehors |
| prets.mois | Une date comme 2026-03-01 | De 1 à 12, cette année |
| livres.annee | Année de l’achat | Année de la parution |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent qu’un modèle connaît déjà leur base. Il ne l’a jamais vue. Envoyez les tables, les jointures et les notes avec chaque question, car chaque requête repart de zéro.
Une description courte plutôt que tout le CREATE
Vous pourriez coller chaque instruction CREATE TABLE. Une ligne courte par table, puis les jointures et les notes, dit la même chose sous une forme facile à lire.
Dans une base de quarante tables, n’envoyez que les tables utiles à la question. Moins de mots coûtent moins cher et laissent moins de place aux erreurs de lecture.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' '// Une réponse peut entourer la requête d’un bloc délimité : on garde l’intérieur.' 'function sqlDe(texte) {' ' const morceaux = texte.split(String.fromCharCode(96).repeat(3));' ' const dedans = (morceaux.length > 2 ? morceaux[1] : texte).trim();' ' return dedans.toLowerCase().startsWith(\"sql\") ? dedans.slice(3).trim() : dedans;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander, sqlDe };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la base de la médiathèque, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Une médiathèque inventée : six livres, quatre membres, neuf prêts.' 'const LIVRES = [[1, \"Le Phare des oranges\", 2019], [2, \"Port de verre\", 2021], [3, \"Carte de la pluie\", 2015],' ' [4, \"Moteurs tranquilles\", 2023], [5, \"Sept Bouilloires\", 2018], [6, \"Au nord de midi\", 2022]];' 'const MEMBRES = [[1, \"Amina\"], [2, \"Bruno\"], [3, \"Chloé\"], [4, \"Driss\"]];' '// id, livre_id, membre_id, mois, rendu (1 rendu, 0 encore dehors)' 'const PRETS = [[1, 1, 1, 3, 1], [2, 2, 1, 3, 1], [3, 3, 2, 3, 0], [4, 1, 3, 3, 1], [5, 1, 1, 4, 0],' ' [6, 4, 2, 4, 0], [7, 2, 3, 4, 1], [8, 5, 2, 5, 0], [9, 2, 4, 5, 0]];' > mediatheque.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la base de la médiathèque, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function remplir(db, table, lignes) {' ' const marques = lignes[0].map(function () { return \"?\"; }).join(\", \");' ' const inserer = db.prepare(\"INSERT INTO \" + table + \" VALUES (\" + marques + \")\");' ' lignes.forEach(function (ligne) { inserer.run(...ligne); });' '}' 'function ouvrir(fichier) {' ' const db = new DatabaseSync(fichier || \":memory:\");' ' db.exec(\"CREATE TABLE livres (id INTEGER PRIMARY KEY, titre TEXT, annee INTEGER)\");' ' db.exec(\"CREATE TABLE membres (id INTEGER PRIMARY KEY, nom TEXT)\");' ' db.exec(\"CREATE TABLE prets (id INTEGER PRIMARY KEY, livre_id INTEGER REFERENCES livres(id), \"' ' + \"membre_id INTEGER REFERENCES membres(id), mois INTEGER, rendu INTEGER)\");' ' remplir(db, \"livres\", LIVRES);' ' remplir(db, \"membres\", MEMBRES);' ' remplir(db, \"prets\", PRETS);' ' return db;' '}' 'module.exports = { ouvrir };' >> mediatheque.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du schéma, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander, sqlDe } = require(\"./model\");' 'const { ouvrir } = require(\"./mediatheque\");' 'const db = ouvrir();' 'const tables = db.prepare(\"SELECT name, sql FROM sqlite_master WHERE type = ? ORDER BY name\").all(\"table\");' 'function court(t) {' ' const cols = db.prepare(\"SELECT name, type FROM pragma_table_info(?)\").all(t.name)' ' .map(function (c) { return c.name + \" \" + c.type; });' ' return t.name + \"(\" + cols.join(\", \") + \")\";' '}' 'const liens = [];' 'tables.forEach(function (t) {' ' db.prepare(\"SELECT * FROM pragma_foreign_key_list(?)\").all(t.name).forEach(function (k) {' ' liens.push(t.name + \".\" + k.from + \" -> \" + k.table + \".\" + k.to);' ' });' '});' > schema.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du schéma, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const notes = [\"prets.rendu vaut 1 quand le livre est rendu, 0 tant qu’il est dehors\", \"prets.mois va de 1 à 12, cette année\"];' 'const schema = tables.map(court).concat(liens, notes).join(\"\n\");' 'const complet = tables.map(function (t) { return t.sql; }).join(\"\n\");' 'console.log(schema);' 'console.log(\"texte CREATE complet : \" + complet.length + \" caractères, version courte avec notes : \" + schema.length);' >> schema.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script du schéma, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const question = \"Combien de prêts ont été faits au mois 3 ?\";' ' const systeme = \"Répondez par une seule requête SELECT pour SQLite, et rien d’autre.\";' ' const echantillon = \"SELECT COUNT(*) FROM prets WHERE mois = 3;\";' ' const reponse = await demander([{ role: \"system\", content: systeme },' ' { role: \"user\", content: schema + \"\nQuestion : \" + question }], echantillon, 120);' ' console.log(\"le modèle a écrit : \" + sqlDe(reponse.texte));' ' console.log(\"pas encore vérifiée : les leçons 2 à 5 la vérifient\");' '}' 'main();' >> schema.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Décrire le schéma et demander
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings schema.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| livres(id INTEGER, titre TEXT, annee INTEGER) | Une ligne courte par table |
| prets.livre_id -> livres.id | Une ligne par jointure, lue dans les clés étrangères |
| prets.rendu vaut 1 quand le livre est rendu, 0 tant qu’il est dehors | Une note écrite à la main |
| texte CREATE complet : 282 caractères, version courte avec notes : 323 | La taille des deux formes |
| le modèle a écrit : SELECT COUNT(*) FROM prets WHERE mois = 3; | La requête, ou un échantillon enregistré si aucun modèle ne répond |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.