Warum Modell-SQL nur lesen darf
Modell-SQL sicher auszuführen heißt, dass es lesen und nie etwas ändern kann. Ein Modell könnte DELETE oder DROP aus Versehen schreiben, oder weil eine Frage es hereingelegt hat.
Modul 4 lehrt, Daten absichtlich zu ändern. Diese Lektion stellt drei Schlösser vor die Abfrage eines Modells: einen Wächter, Limits und eine Verbindung, die nur lesen kann.
Am Ende dieser Lektion
Notiz
Ich kann die Abfrage eines Modells so ausführen, dass sie nur lesen kann, und zwar wenig auf einmal.
Schlüsselwörter
| Begriff | Was er bedeutet | Beispiel |
|---|---|---|
| Wächter | Code, der eine Abfrage vor dem Ausführen ablehnt | |
| Anweisung | ein vollständiger SQL-Befehl | SELECT titel FROM buecher |
| Zeilenlimit | die höchste Zahl Zeilen, die zurückkommen darf | |
| Zeitlimit | die längste Zeit, die eine Abfrage laufen darf | |
| Nur lesend | darf Zeilen lesen, aber nie ändern |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Genau ein SELECT erlauben, sonst nichts
Der Wächter räumt die Abfrage auf und streicht ein Semikolon am Schluss. Bleibt ein weiteres Semikolon übrig, gibt es eine zweite Anweisung, also lehnt er ab.
Das erste Wort muss SELECT oder WITH sein. Jedes schreibende Wort wie DELETE, DROP oder UPDATE wird abgelehnt, wo immer es steht. Ebenso ein Kommentar, der Text verstecken kann.
Beispiel: was der Wächter sagt
| Abfrage | Urteil | Grund |
|---|---|---|
| SELECT titel FROM buecher WHERE jahr > 2020 | Erlaubt | Ein SELECT |
| SELECT titel FROM buecher; DROP TABLE ausleihen | Abgelehnt | Mehr als eine Anweisung |
| DELETE FROM ausleihen WHERE zurueck = 1 | Abgelehnt | Kein SELECT |
| SELECT titel FROM buecher -- Notiz | Abgelehnt | Ein Kommentar |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Ein Zeilenlimit und ein Zeitlimit
Auch eine richtige Abfrage kann eine Million Zeilen liefern oder endlos laufen. Packen Sie sie in SELECT * FROM (die Abfrage) LIMIT 5, dann kommen höchstens fünf Zeilen zurück.
Führen Sie sie in einem eigenen Prozess mit Zeitlimit aus. Läuft die Zeit ab, wird der Prozess gestoppt, und die Seite antwortet, statt zu hängen.
Lesender Zugriff ist das letzte Schloss
Ein Wächter prüft Wörter, und Wörter können durchrutschen. Öffnen Sie die Datenbank deshalb nur lesend, oder melden Sie sich als Benutzer an, der nur SELECT darf.
Führen Sie Modell-SQL nie mit Schreibrechten auf Live-Daten aus. Nutzen Sie eine Kopie, die nur lesbar ist, dann ist das Schlimmste, was eine schlechte Abfrage tun kann, langsam zu sein.
Ein häufiger Fehler
Warnung
Viele denken, ein Wächter, der die Wörter prüft, genüge. Ein Wächter hat Lücken, weil SQL viele Wege kennt, dasselbe zu sagen. Behalten Sie auch die Verbindung, die nur lesen kann, damit eine übersehene Abfrage trotzdem nicht schreibt.
Die Modellhilfe schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const basis = process.env.AI_URL || \"http://ai:4000\";' '// Die Token einer Antwort oder null, wenn sie keine nennt.' 'function zaehlen(daten, name) {' ' return daten.usage && typeof daten.usage[name] === \"number\" ? daten.usage[name] : null;' '}' '// Eine Antwort kann die Abfrage in einen umzäunten Block setzen: Wir behalten das Innere.' 'function sqlVon(text) {' ' const teile = text.split(String.fromCharCode(96).repeat(3));' ' const innen = (teile.length > 2 ? teile[1] : text).trim();' ' return innen.toLowerCase().startsWith(\"sql\") ? innen.slice(3).trim() : innen;' '}' > model.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Die Modellhilfe schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function fragen(nachrichten, probe, max) {' ' try {' ' const res = await fetch(basis + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: nachrichten, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"Status \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, eingabe: zaehlen(d, \"prompt_tokens\"), ausgabe: zaehlen(d, \"completion_tokens\"), aufgezeichnet: false };' ' } catch (fehler) {' ' console.log(\"Aufgezeichnete Probe: Kein Modell hat geantwortet (\" + fehler.message + \").\");' ' return { text: probe, eingabe: null, ausgabe: null, aufgezeichnet: true };' ' }' '}' 'module.exports = { fragen, sqlVon };' >> model.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Die kleine Bibliotheksdatenbank schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Eine erfundene Bibliothek: sechs Bücher, vier Mitglieder, neun Ausleihen.' 'const BUECHER = [[1, \"Der Salzweg\", 2019], [2, \"Hafen aus Glas\", 2021], [3, \"Eine Karte aus Regen\", 2015],' ' [4, \"Leise Maschinen\", 2023], [5, \"Sieben Kessel\", 2018], [6, \"Nördlich vom Mittag\", 2022]];' 'const MITGLIEDER = [[1, \"Anke\"], [2, \"Bernd\"], [3, \"Carla\"], [4, \"Dirk\"]];' '// id, buch_id, mitglied_id, monat, zurueck (1 zurück, 0 noch draußen)' 'const AUSLEIHEN = [[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]];' > bibliothek.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Die kleine Bibliotheksdatenbank schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function fuellen(db, tabelle, zeilen) {' ' const marken = zeilen[0].map(function () { return \"?\"; }).join(\", \");' ' const einfuegen = db.prepare(\"INSERT INTO \" + tabelle + \" VALUES (\" + marken + \")\");' ' zeilen.forEach(function (zeile) { einfuegen.run(...zeile); });' '}' 'function oeffnen(datei) {' ' const db = new DatabaseSync(datei || \":memory:\");' ' db.exec(\"CREATE TABLE buecher (id INTEGER PRIMARY KEY, titel TEXT, jahr INTEGER)\");' ' db.exec(\"CREATE TABLE mitglieder (id INTEGER PRIMARY KEY, name TEXT)\");' ' db.exec(\"CREATE TABLE ausleihen (id INTEGER PRIMARY KEY, buch_id INTEGER REFERENCES buecher(id), \"' ' + \"mitglied_id INTEGER REFERENCES mitglieder(id), monat INTEGER, zurueck INTEGER)\");' ' fuellen(db, \"buecher\", BUECHER);' ' fuellen(db, \"mitglieder\", MITGLIEDER);' ' fuellen(db, \"ausleihen\", AUSLEIHEN);' ' return db;' '}' 'module.exports = { oeffnen };' >> bibliothek.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Den nur lesenden Ausführer schreiben
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Nur lesend geöffnet: Auch eine Abfrage, die der Wächter übersehen hat, kann keine Zeile ändern.' 'const db = new DatabaseSync(\"bibliothek.db\", { readOnly: true });' 'try {' ' const zeilen = db.prepare(process.argv[2]).all().map(function (r) { return Object.values(r); });' ' console.log(\"Zeilen \" + zeilen.length + \": \" + JSON.stringify(zeilen));' '} catch (fehler) { console.log(\"Fehler: \" + fehler.message); }' > einzeln.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Das Skript mit dem Wächter schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { fragen, sqlVon } = require(\"./model\");' 'const { oeffnen } = require(\"./bibliothek\");' 'const fs = require(\"node:fs\");' 'const { spawnSync } = require(\"node:child_process\");' 'fs.rmSync(\"bibliothek.db\", { force: true });' 'oeffnen(\"bibliothek.db\").close();' 'const VERBOTEN = [\"INSERT\", \"UPDATE\", \"DELETE\", \"DROP\", \"ALTER\", \"CREATE\", \"ATTACH\", \"PRAGMA\", \"REPLACE\"];' 'const LIMIT = 5;' > waechter.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Das Skript mit dem Wächter schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function waechter(sql) {' ' let text = sql.trim();' ' if (text.endsWith(\";\")) text = text.slice(0, -1).trim();' ' if (text.indexOf(\";\") >= 0) return { ok: false, grund: \"mehr als eine Anweisung\" };' ' if (text.indexOf(\"--\") >= 0 || text.indexOf(\"/*\") >= 0) return { ok: false, grund: \"ein Kommentar\" };' ' const woerter = text.toUpperCase().split(/[^A-Z_]+/).filter(function (w) { return w.length > 0; });' ' if ([\"SELECT\", \"WITH\"].indexOf(woerter[0]) < 0) return { ok: false, grund: \"kein SELECT\" };' ' const boese = woerter.filter(function (w) { return VERBOTEN.indexOf(w) >= 0; });' ' if (boese.length) return { ok: false, grund: \"verbotenes Wort \" + boese[0] };' ' return { ok: true, sql: \"SELECT * FROM (\" + text + \") LIMIT \" + LIMIT };' '}' >> waechter.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Das Skript mit dem Wächter schreiben, Teil 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function einzeln(sql) {' ' const aus = spawnSync(process.execPath, [\"--experimental-sqlite\", \"--no-warnings\", \"einzeln.js\", sql],' ' { encoding: \"utf8\", timeout: 2000 });' ' return aus.error ? \"gestoppt: über 2 Sekunden\" : aus.stdout.trim();' '}' 'function ausfuehren(sql) {' ' const w = waechter(sql);' ' return w.ok ? einzeln(w.sql) : \"abgelehnt: \" + w.grund;' '}' 'const N = \"WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n\";' >> waechter.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Das Skript mit dem Wächter schreiben, Teil 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const versuche = [\"SELECT titel FROM buecher WHERE jahr > 2020\", \"SELECT titel FROM buecher; DROP TABLE ausleihen\",' ' \"DELETE FROM ausleihen WHERE zurueck = 1\", N + \") SELECT COUNT(*) FROM n\",' ' N + \" WHERE i < 1000) SELECT i FROM n\"];' ' versuche.forEach(function (sql, i) { console.log((i + 1) + \" \" + ausfuehren(sql)); });' ' console.log(\"6 ohne Wächter, Datei nur lesend: \" + einzeln(\"DELETE FROM ausleihen\"));' ' await live();' '}' >> waechter.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Das Skript mit dem Wächter schreiben, Teil 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function live() {' ' const f = \"buecher(id, titel, jahr)\nFrage: Welche Bücher sind nach 2020 erschienen?\";' ' const probe = \"SELECT titel FROM buecher WHERE jahr > 2020;\";' ' const antwort = await fragen([{ role: \"system\", content: \"Antworten Sie mit genau einer SQLite-SELECT-Abfrage und sonst nichts.\" },' ' { role: \"user\", content: f }], probe, 120);' ' const sql = sqlVon(antwort.text);' ' console.log(\"Das Modell schrieb: \" + sql);' ' console.log(\" durch den Wächter: \" + ausfuehren(sql));' '}' 'main();' >> waechter.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Sechs Abfragen durch die Schlösser schicken
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings waechter.jsHier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Was der Run ausgibt
| Zeile | Was sie zeigt |
|---|---|
| 1 Zeilen 3: [["Hafen aus Glas"],["Leise Maschinen"],["Nördlich vom Mittag"]] | Eine erlaubte Abfrage |
| 2 abgelehnt: mehr als eine Anweisung | Ein verstecktes DROP |
| 3 abgelehnt: kein SELECT | Ein Schreibversuch |
| 4 gestoppt: über 2 Sekunden | Eine Abfrage, die nie endet |
| 5 Zeilen 5: [[1],[2],[3],[4],[5]] | 1.000 Zeilen auf fünf gekürzt |
| 6 ohne Wächter, Datei nur lesend: Fehler: attempt to write a readonly database | Das letzte Schloss hält |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.