Lo que el texto a SQL necesita saber
El texto a SQL es un modelo de lenguaje que convierte una pregunta en palabras normales en una consulta SQL. El modelo nunca ha visto tu base de datos, así que adivina las tablas que no describes.
El módulo 4 enseña SQL y el módulo 63 pide a un modelo una forma fija. Esta lección decide qué contarle al modelo sobre tus tablas.
Al final de esta lección
Nota
Puedo describir el esquema de una base de datos de forma breve para que un modelo escriba SQL a partir de él.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Texto a SQL | un modelo que convierte una pregunta en una consulta | |
| Esquema | las tablas y columnas de una base de datos | |
| Clave foránea | una columna que señala una fila de otra tabla | loans.book_id |
| Unión | emparejar las filas de dos tablas por una columna | |
| Nota | una línea que dice qué significan los valores de una columna |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Lo mínimo: tablas, columnas y uniones
Para escribir una consulta, el modelo necesita tres cosas. Necesita el nombre de las tablas, las columnas de cada una y cómo se unen las tablas entre sí.
Sin las uniones, adivina. Unir loans.id con books.id, por ejemplo, funciona sin error y devuelve los libros equivocados. Los nombres de tablas y columnas van en inglés, como los identificadores del código.
Ejemplo resuelto: el esquema de la biblioteca
| Tabla | Columnas | Se une con |
|---|---|---|
| books | id, title, year | Nada |
| members | id, name | Nada |
| loans | id, book_id, member_id, month, returned | book_id con books.id, member_id con members.id |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Algunas columnas necesitan una nota
El nombre de una columna no siempre dice qué significan sus valores. En la biblioteca, returned vale 1 cuando el libro ha vuelto y 0 mientras sigue fuera.
Un modelo que solo lee el nombre podría probar returned = «no» o returned IS NULL. Una nota corta para cada columna poco clara evita esa suposición.
Ejemplo resuelto: nombres que piden una nota
| Columna | Una suposición sin nota | La nota |
|---|---|---|
| loans.returned | returned = «no» | 1 si ha vuelto, 0 si sigue fuera |
| loans.month | Una fecha como 2026-03-01 | De 1 a 12, de este año |
| books.year | Año de la compra | Año de publicación |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un error frecuente
Advertencia
Mucha gente cree que el modelo ya conoce su base de datos. Nunca la ha visto. Envía las tablas, las uniones y las notas con cada pregunta, porque cada petición empieza de cero.
Una descripción breve mejor que todo el CREATE
Podrías pegar cada sentencia CREATE TABLE. Una línea corta por tabla, seguida de las uniones y las notas, dice lo mismo de una forma fácil de leer.
En una base de cuarenta tablas, envía solo las que necesita la pregunta. Menos palabras cuestan menos y dejan menos sitio a errores de lectura.
Escribe el ayudante del modelo, parte 1
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
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
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
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 script del esquema, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask, sqlOf } = require(\"./model\");' 'const { open } = require(\"./library\");' 'const db = open();' 'const tables = db.prepare(\"SELECT name, sql FROM sqlite_master WHERE type = ? ORDER BY name\").all(\"table\");' 'function brief(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 links = [];' 'tables.forEach(function (t) {' ' db.prepare(\"SELECT * FROM pragma_foreign_key_list(?)\").all(t.name).forEach(function (k) {' ' links.push(t.name + \".\" + k.from + \" -> \" + k.table + \".\" + k.to);' ' });' '});' > schema.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 esquema, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const notes = [\"loans.returned vale 1 cuando el libro ha vuelto, 0 mientras sigue fuera\", \"loans.month va de 1 a 12, de este año\"];' 'const schema = tables.map(brief).concat(links, notes).join(\"\n\");' 'const whole = tables.map(function (t) { return t.sql; }).join(\"\n\");' 'console.log(schema);' 'console.log(\"texto CREATE completo: \" + whole.length + \" caracteres, versión breve con notas: \" + schema.length);' >> schema.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 esquema, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const question = \"¿Cuántos préstamos se hicieron en el mes 3?\";' ' const system = \"Responde con una sola consulta SELECT de SQLite y nada más.\";' ' const sample = \"SELECT COUNT(*) FROM loans WHERE month = 3;\";' ' const reply = await ask([{ role: \"system\", content: system },' ' { role: \"user\", content: schema + \"\nPregunta: \" + question }], sample, 120);' ' console.log(\"el modelo escribió: \" + sqlOf(reply.text));' ' console.log(\"aún sin comprobar: la comprueban las lecciones 2 a 5\");' '}' 'main();' >> schema.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Describe el esquema y pregunta
docker exec -w /home/node/project agent node --experimental-sqlite --no-warnings schema.jsSe 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ínea | Qué muestra |
|---|---|
| books(id INTEGER, title TEXT, year INTEGER) | Una línea corta por tabla |
| loans.book_id -> books.id | Una línea por unión, leída de las claves foráneas |
| loans.returned vale 1 cuando el libro ha vuelto, 0 mientras sigue fuera | Una nota escrita a mano |
| texto CREATE completo: 283 caracteres, versión breve con notas: 327 | El tamaño de las dos formas |
| el modelo escribió: SELECT COUNT(*) FROM loans WHERE month = 3; | La consulta, o una muestra grabada si ningún modelo responde |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.