¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

SQL de un modelo: solo lectura y límites

Después de esta lección: Puedo ejecutar la consulta de un modelo de forma que solo pueda leer, poco a poco.

Lección de 70 min11 comandosLaboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

Por qué el SQL del modelo solo lee

Ejecutar sin peligro el SQL de un modelo es dejarle leer sin cambiar nunca nada. Un modelo puede escribir DELETE o DROP por error, o porque la pregunta de un usuario lo ha engañado.

El módulo 4 enseña a cambiar datos a propósito. Esta lección pone tres cerrojos delante de la consulta de un modelo: un guardián, unos límites y una conexión de solo lectura.

Al final de esta lección

Nota

Puedo ejecutar la consulta de un modelo de forma que solo pueda leer, poco a poco.

Palabras clave

PalabraQué significaEjemplo
Guardiáncódigo que rechaza una consulta antes de que se ejecute
Sentenciauna orden SQL completaSELECT title FROM books
Límite de filasel mayor número de filas que puede devolver una consulta
Tiempo máximolo más que puede durar una consulta
Solo lecturapoder leer filas sin cambiarlas nunca

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Acepta un solo SELECT y nada más

El guardián ordena la consulta y quita un punto y coma final. Si aún queda un punto y coma, hay una segunda sentencia, y la rechaza.

La primera palabra tiene que ser SELECT o WITH. Cualquier palabra de escritura, como DELETE, DROP o UPDATE, se rechaza esté donde esté. También un comentario, porque puede esconder texto.

Ejemplo resuelto: lo que dice el guardián

ConsultaVeredictoMotivo
SELECT title FROM books WHERE year > 2020AceptadaUn solo SELECT
SELECT title FROM books; DROP TABLE loansRechazadaMás de una sentencia
DELETE FROM loans WHERE returned = 1RechazadaNo es un SELECT
SELECT title FROM books -- notaRechazadaUn comentario

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Un límite de filas y de tiempo

Una consulta correcta puede devolver un millón de filas o no terminar nunca. Envuélvela así: SELECT * FROM (la consulta) LIMIT 5. Vuelven como mucho cinco filas.

Ejecútala en un proceso aparte, con un tiempo máximo. Cuando se acaba el tiempo, el proceso se detiene y la página responde en lugar de quedarse colgada.

La solo lectura es el último cerrojo

Un guardián comprueba palabras, y algunas palabras se pueden colar. Por eso abre la base en solo lectura, o conéctate con una cuenta que solo tenga permiso de SELECT.

No ejecutes nunca el SQL de un modelo sobre datos reales con permiso de escritura. Usa una copia de solo lectura: lo peor que puede hacer una mala consulta es tardar.

Un error frecuente

Advertencia

Mucha gente cree que basta con un guardián que comprueba palabras. Un guardián tiene huecos, porque el SQL tiene mil formas de decir lo mismo. Mantén también la conexión en solo lectura: así una consulta que se escape seguirá sin poder escribir.

Escribe el ayudante del modelo, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' '  return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' '// Una respuesta puede envolver la consulta en un bloque delimitado: nos quedamos con el interior.' 'function sqlOf(text) {' '  const parts = text.split(String.fromCharCode(96).repeat(3));' '  const inner = (parts.length > 2 ? parts[1] : text).trim();' '  return inner.toLowerCase().startsWith(\"sql\") ? inner.slice(3).trim() : inner;' '}' > model.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el ayudante del modelo, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, 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(\"estado \" + res.status);' '    const d = await res.json();' '    return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' '  } catch (error) {' '    console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' '    return { text: sample, inTokens: null, outTokens: null, recorded: true };' '  }' '}' 'module.exports = { ask, sqlOf };' >> model.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe la base de la biblioteca, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Una biblioteca inventada: seis libros, cuatro socios, nueve préstamos.' 'const BOOKS = [[1, \"La calle de la sal\", 2019], [2, \"Puerto de cristal\", 2021], [3, \"Mapa de la lluvia\", 2015],' '  [4, \"Motores en calma\", 2023], [5, \"Siete teteras\", 2018], [6, \"Al norte del mediodía\", 2022]];' 'const MEMBERS = [[1, \"Alba\"], [2, \"Bruno\"], [3, \"Carmen\"], [4, \"Diego\"]];' '// id, book_id, member_id, month, returned (1 devuelto, 0 aún fuera)' 'const LOANS = [[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]];' > library.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe la base de la biblioteca, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function put(db, table, rows) {' '  const marks = rows[0].map(function () { return \"?\"; }).join(\", \");' '  const insert = db.prepare(\"INSERT INTO \" + table + \" VALUES (\" + marks + \")\");' '  rows.forEach(function (row) { insert.run(...row); });' '}' 'function open(file) {' '  const db = new DatabaseSync(file || \":memory:\");' '  db.exec(\"CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, year INTEGER)\");' '  db.exec(\"CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT)\");' '  db.exec(\"CREATE TABLE loans (id INTEGER PRIMARY KEY, book_id INTEGER REFERENCES books(id), \"' '    + \"member_id INTEGER REFERENCES members(id), month INTEGER, returned INTEGER)\");' '  put(db, \"books\", BOOKS);' '  put(db, \"members\", MEMBERS);' '  put(db, \"loans\", LOANS);' '  return db;' '}' 'module.exports = { open };' >> library.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el ejecutor de solo lectura

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { DatabaseSync } = require(\"node:sqlite\");' '// Abierta en solo lectura: ni una consulta que se le escape al guardián puede cambiar una fila.' 'const db = new DatabaseSync(\"library.db\", { readOnly: true });' 'try {' '  const rows = db.prepare(process.argv[2]).all().map(function (r) { return Object.values(r); });' '  console.log(\"filas \" + rows.length + \": \" + JSON.stringify(rows));' '} catch (error) { console.log(\"error: \" + error.message); }' > runone.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el script del guardián, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask, sqlOf } = require(\"./model\");' 'const { open } = require(\"./library\");' 'const fs = require(\"node:fs\");' 'const { spawnSync } = require(\"node:child_process\");' 'fs.rmSync(\"library.db\", { force: true });' 'open(\"library.db\").close();' 'const BANNED = [\"INSERT\", \"UPDATE\", \"DELETE\", \"DROP\", \"ALTER\", \"CREATE\", \"ATTACH\", \"PRAGMA\", \"REPLACE\"];' 'const LIMIT = 5;' > guard.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el script del guardián, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function guard(sql) {' '  let text = sql.trim();' '  if (text.endsWith(\";\")) text = text.slice(0, -1).trim();' '  if (text.indexOf(\";\") >= 0) return { ok: false, why: \"más de una sentencia\" };' '  if (text.indexOf(\"--\") >= 0 || text.indexOf(\"/*\") >= 0) return { ok: false, why: \"un comentario\" };' '  const words = text.toUpperCase().split(/[^A-Z_]+/).filter(function (w) { return w.length > 0; });' '  if ([\"SELECT\", \"WITH\"].indexOf(words[0]) < 0) return { ok: false, why: \"no es un SELECT\" };' '  const bad = words.filter(function (w) { return BANNED.indexOf(w) >= 0; });' '  if (bad.length) return { ok: false, why: \"palabra prohibida \" + bad[0] };' '  return { ok: true, sql: \"SELECT * FROM (\" + text + \") LIMIT \" + LIMIT };' '}' >> guard.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el script del guardián, parte 3

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function runOne(sql) {' '  const out = spawnSync(process.execPath, [\"--experimental-sqlite\", \"--no-warnings\", \"runone.js\", sql],' '    { encoding: \"utf8\", timeout: 2000 });' '  return out.error ? \"detenida: más de 2 segundos\" : out.stdout.trim();' '}' 'function run(sql) {' '  const g = guard(sql);' '  return g.ok ? runOne(g.sql) : \"rechazada: \" + g.why;' '}' 'const N = \"WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n\";' >> guard.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el script del guardián, parte 4

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const tries = [\"SELECT title FROM books WHERE year > 2020\", \"SELECT title FROM books; DROP TABLE loans\",' '    \"DELETE FROM loans WHERE returned = 1\", N + \") SELECT COUNT(*) FROM n\",' '    N + \" WHERE i < 1000) SELECT i FROM n\"];' '  tries.forEach(function (sql, i) { console.log((i + 1) + \" \" + run(sql)); });' '  console.log(\"6 sin guardián, archivo de solo lectura: \" + runOne(\"DELETE FROM loans\"));' '  await live();' '}' >> guard.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el script del guardián, parte 5

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function live() {' '  const q = \"books(id, title, year)\nPregunta: ¿Qué libros se publicaron después de 2020?\";' '  const sample = \"SELECT title FROM books WHERE year > 2020;\";' '  const reply = await ask([{ role: \"system\", content: \"Responde con una sola consulta SELECT de SQLite y nada más.\" },' '    { role: \"user\", content: q }], sample, 120);' '  const sql = sqlOf(reply.text);' '  console.log(\"el modelo escribió: \" + sql);' '  console.log(\"  tras el guardián: \" + run(sql));' '}' 'main();' >> guard.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Pasa seis consultas por los cerrojos

Comando
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings guard.js

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Lo que imprime el Run

LíneaQué muestra
1 filas 3: [["Puerto de cristal"],["Motores en calma"],["Al norte del mediodía"]]Una consulta aceptada
2 rechazada: más de una sentenciaUn DROP escondido
3 rechazada: no es un SELECTUna escritura
4 detenida: más de 2 segundosUna consulta que no termina nunca
5 filas 5: [[1],[2],[3],[4],[5]]1000 filas reducidas a cinco
6 sin guardián, archivo de solo lectura: error: attempt to write a readonly databaseEl último cerrojo aguanta

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Sobre este laboratorio y su código: Laboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.