Plus facile à lire avec un fond crème, une teinte, plus de contraste ou un texte plus grand ? Choisissez vos couleurs et votre texte, une fois pour toutes les pages.

Leçon gratuite

SQL d’un modèle : lecture seule et limites

Après cette leçon: Je peux lancer la requête d’un modèle pour qu’elle ne fasse que lire, un peu à la fois.

Leçon de 70 min11 commandesLab agent de code IA : utiliser opencode pour expliquer et corriger du code, et vérifier son travail

Pourquoi le SQL d’un modèle doit seulement lire

Lancer le SQL d’un modèle sans danger, c’est lui permettre de lire sans jamais rien changer. Un modèle peut écrire DELETE ou DROP par erreur, ou parce que la question d’un utilisateur l’a piégé.

Le module 4 enseigne à changer des données exprès. Cette leçon place trois verrous devant la requête d’un modèle : une garde, des limites et une connexion en lecture seule.

À la fin de cette leçon

Remarque

Je peux lancer la requête d’un modèle pour qu’elle ne fasse que lire, un peu à la fois.

Mots clés

MotCe que ça veut direExemple
Gardedu code qui refuse une requête avant qu’elle tourne
Instructionune commande SQL complèteSELECT titre FROM livres
Limite de lignesle plus grand nombre de lignes qu’une requête peut renvoyer
Délai maximalle plus long temps qu’une requête peut tourner
Lecture seulepouvoir lire des lignes sans jamais les changer

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

N’accepter qu’un SELECT, et rien d’autre

La garde range la requête et retire un point-virgule final. S’il reste un point-virgule, il y a une seconde instruction : elle refuse.

Le premier mot doit être SELECT ou WITH. Tout mot d’écriture, comme DELETE, DROP ou UPDATE, est refusé où qu’il soit. Un commentaire aussi, car il peut cacher du texte.

Exemple détaillé : ce que dit la garde

RequêteVerdictRaison
SELECT titre FROM livres WHERE annee > 2020AcceptéeUn seul SELECT
SELECT titre FROM livres; DROP TABLE pretsRefuséePlus d’une instruction
DELETE FROM prets WHERE rendu = 1RefuséePas un SELECT
SELECT titre FROM livres -- noteRefuséeUn commentaire

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

Une limite de lignes et de temps

Une requête juste peut quand même renvoyer un million de lignes ou tourner sans fin. Entourez-la ainsi : SELECT * FROM (la requête) LIMIT 5. Au plus cinq lignes reviennent.

Lancez-la dans un processus à part, avec un délai maximal. Quand le temps est écoulé, le processus est arrêté et la page répond au lieu de rester bloquée.

La lecture seule est le dernier verrou

Une garde contrôle des mots, et des mots peuvent passer à travers. Ouvrez donc la base en lecture seule, ou connectez-vous avec un compte qui n’a droit qu’à SELECT.

Ne lancez jamais le SQL d’un modèle sur des données réelles avec un droit d’écriture. Utilisez une copie en lecture seule : le pire qu’une mauvaise requête puisse faire, c’est être lente.

Une erreur courante

Attention

Beaucoup pensent qu’une garde qui contrôle les mots suffit. Une garde a des trous, car le SQL a mille façons de dire une chose. Gardez aussi la connexion en lecture seule : une requête ratée ne pourra toujours pas écrire.

Écrire l’assistant du modèle, partie 1

Commande
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

Commande
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

Commande
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

Commande
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 lanceur en lecture seule

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Ouverte en lecture seule : même une requête ratée par la garde ne peut changer aucune ligne.' 'const db = new DatabaseSync(\"mediatheque.db\", { readOnly: true });' 'try {' '  const lignes = db.prepare(process.argv[2]).all().map(function (r) { return Object.values(r); });' '  console.log(\"lignes \" + lignes.length + \" : \" + JSON.stringify(lignes));' '} catch (erreur) { console.log(\"erreur : \" + erreur.message); }' > lancer.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 de la garde, partie 1

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander, sqlDe } = require(\"./model\");' 'const { ouvrir } = require(\"./mediatheque\");' 'const fs = require(\"node:fs\");' 'const { spawnSync } = require(\"node:child_process\");' 'fs.rmSync(\"mediatheque.db\", { force: true });' 'ouvrir(\"mediatheque.db\").close();' 'const INTERDITS = [\"INSERT\", \"UPDATE\", \"DELETE\", \"DROP\", \"ALTER\", \"CREATE\", \"ATTACH\", \"PRAGMA\", \"REPLACE\"];' 'const LIMITE = 5;' > garde.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 de la garde, partie 2

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function garde(sql) {' '  let texte = sql.trim();' '  if (texte.endsWith(\";\")) texte = texte.slice(0, -1).trim();' '  if (texte.indexOf(\";\") >= 0) return { ok: false, raison: \"plus d’une instruction\" };' '  if (texte.indexOf(\"--\") >= 0 || texte.indexOf(\"/*\") >= 0) return { ok: false, raison: \"un commentaire\" };' '  const mots = texte.toUpperCase().split(/[^A-Z_]+/).filter(function (m) { return m.length > 0; });' '  if ([\"SELECT\", \"WITH\"].indexOf(mots[0]) < 0) return { ok: false, raison: \"pas un SELECT\" };' '  const mauvais = mots.filter(function (m) { return INTERDITS.indexOf(m) >= 0; });' '  if (mauvais.length) return { ok: false, raison: \"mot interdit \" + mauvais[0] };' '  return { ok: true, sql: \"SELECT * FROM (\" + texte + \") LIMIT \" + LIMITE };' '}' >> garde.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 de la garde, partie 3

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function lancerUne(sql) {' '  const res = spawnSync(process.execPath, [\"--experimental-sqlite\", \"--no-warnings\", \"lancer.js\", sql],' '    { encoding: \"utf8\", timeout: 2000 });' '  return res.error ? \"arrêtée : plus de 2 secondes\" : res.stdout.trim();' '}' 'function lancer(sql) {' '  const g = garde(sql);' '  return g.ok ? lancerUne(g.sql) : \"refusée : \" + g.raison;' '}' 'const N = \"WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n\";' >> garde.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 de la garde, partie 4

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const essais = [\"SELECT titre FROM livres WHERE annee > 2020\", \"SELECT titre FROM livres; DROP TABLE prets\",' '    \"DELETE FROM prets WHERE rendu = 1\", N + \") SELECT COUNT(*) FROM n\",' '    N + \" WHERE i < 1000) SELECT i FROM n\"];' '  essais.forEach(function (sql, i) { console.log((i + 1) + \" \" + lancer(sql)); });' '  console.log(\"6 sans garde, fichier en lecture seule : \" + lancerUne(\"DELETE FROM prets\"));' '  await direct();' '}' >> garde.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 de la garde, partie 5

Commande
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function direct() {' '  const q = \"livres(id, titre, annee)\nQuestion : Quels livres sont parus après 2020 ?\";' '  const echantillon = \"SELECT titre FROM livres WHERE annee > 2020;\";' '  const reponse = await demander([{ role: \"system\", content: \"Répondez par une seule requête SELECT pour SQLite, et rien d’autre.\" },' '    { role: \"user\", content: q }], echantillon, 120);' '  const sql = sqlDe(reponse.texte);' '  console.log(\"le modèle a écrit : \" + sql);' '  console.log(\"  passée par la garde : \" + lancer(sql));' '}' 'main();' >> garde.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.

Faire passer six requêtes par les verrous

Commande
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings garde.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.

Ce que le Run affiche

LigneCe qu’elle montre
1 lignes 3 : [["Port de verre"],["Moteurs tranquilles"],["Au nord de midi"]]Une requête acceptée
2 refusée : plus d’une instructionUn DROP caché
3 refusée : pas un SELECTUne écriture
4 arrêtée : plus de 2 secondesUne requête qui ne finit jamais
5 lignes 5 : [[1],[2],[3],[4],[5]]1 000 lignes ramenées à cinq
6 sans garde, fichier en lecture seule : erreur : attempt to write a readonly databaseLe dernier verrou tient

Les cellules que les étudiants remplissent restent vides dans cet aperçu.

À propos de ce labo et de sa source: Lab agent de code IA : utiliser opencode pour expliquer et corriger du code, et vérifier son travail

Vous enseignez à une classe ? Proposez ce module à vos étudiants avec un guide de l'enseignant, des devoirs corrigés et un laboratoire pour chaque étudiant.