Funciones, Procedimientos y Triggers en Vivo
Otra vez PostgreSQL de verdad, compilado a WebAssembly, corriendo enteramente en tu navegador. Esta vez vas a escribir tus propias funciones y procedimientos en PL/pgSQL, y —el momento fuerte del taller— vas a crear un trigger y verlo dispararse solo, sin que vos lo llames, apenas cambie un dato en tu propia base de datos.
Bienvenido
Este taller es la práctica de laboratorio de funciones, procedimientos almacenados y triggers en PostgreSQL. No vuelve a explicar la teoría desde cero —asume que ya viste la guía— y en cambio te da un PostgreSQL real para escribir cada función, cada procedimiento, y para comprobar con tus propios ojos que un trigger se dispara solo.
Si todavía no viste la teoría, repasa primero Procedimientos Almacenados, Funciones y Triggers antes de continuar. Este taller usa una única tabla, productos, y le va sumando funciones, procedimientos y triggers encima a medida que avanzás.
Esta página corre su propia base de datos, independiente de cualquier otro taller que hayas abierto antes. Existe solo mientras esta pestaña siga abierta: si recargas la página, la cierras, o navegas a otra guía, se pierde todo y hay que empezar de nuevo desde "Prepara tu base de datos". Si te equivocás y querés reiniciar sin recargar, usa el botón "Reiniciar base de datos" de abajo.
✍️ Lo escribís vos
Cada bloque parte vacío. El enunciado te dice qué tenés que lograr — el código SQL/PL/pgSQL lo escribís de cero.
⚡ El trigger se dispara solo
En la sección 3 vas a ejecutar un UPDATE que no menciona el trigger en ningún lado, y vas a ver la fila de auditoría aparecer sola.
🧱 Una sola tabla, cada vez más equipada
productos va a terminar el taller con dos funciones, tres procedimientos y dos triggers propios, todos escritos por vos.
Zona de pruebas libres
Este bloque queda disponible durante toda la guía. Usalo en cualquier momento para probar una idea propia, revisar el estado de una tabla con SELECT * FROM tabla;, o experimentar con algo que se te ocurra a mitad de un ejercicio — sin desordenar los bloques guiados.
Prepara tu base de datos
Escribe el CREATE TABLE y los INSERT para productos, con las columnas id, nombre, precio y stock. Cargale estas 5 filas exactas (los ejercicios de todo el taller asumen estos datos):
| nombre | precio | stock |
|---|---|---|
| Mesa | 100.00 | 15 |
| Silla | 40.00 | 5 |
| Lámpara | 25.00 | 0 |
| Estante | 80.00 | 8 |
| Escritorio | 150.00 | 3 |
El SELECT final debería mostrarte 5 filas, en el mismo orden de la tabla de arriba. Si algo no coincide, usa "🔄 Reiniciar base de datos" y volvé a intentar — el resto del taller asume exactamente estos precios y este stock.
1. Funciones almacenadas
Guardá una fórmula una sola vez, adentro de la base de datos, y usala en cualquier SELECT de ahí en más.
Escribe CREATE FUNCTION calcular_precio_final(precio NUMERIC) RETURNS NUMERIC en PL/pgSQL, cuyo cuerpo devuelva ROUND(precio * 1.13, 2) (el ROUND(..., 2) es importante: sin él, NUMERIC te va a dar más decimales de los que esperás). Después, en la misma consulta, tráela usada así: SELECT nombre, precio, calcular_precio_final(precio) AS precio_final FROM productos ORDER BY id;
Deberían salir 5 filas. Mesa debería mostrar 113.00, y Escritorio 169.50 — si te da algo con más decimales (como 113.0000), revisa que hayas usado ROUND(..., 2) adentro de la función.
Escribe clasificar_stock(cantidad INT) RETURNS TEXT, con IF cantidad = 0 THEN RETURN 'Agotado'; ELSIF cantidad < 10 THEN RETURN 'Bajo'; ELSE RETURN 'Suficiente'; END IF; adentro. Después tráela con SELECT nombre, stock, clasificar_stock(stock) AS estado FROM productos ORDER BY id;
Lámpara (stock 0) debería mostrar Agotado; Silla, Estante y Escritorio (menos de 10) Bajo; Mesa (15) Suficiente.
Escribe aplicar_descuento(precio NUMERIC, porcentaje NUMERIC) RETURNS NUMERIC, que devuelva ROUND(precio - (precio * porcentaje / 100), 2). Pruébala sobre Mesa con un 10% de descuento: SELECT nombre, precio, aplicar_descuento(precio, 10) AS precio_con_descuento FROM productos WHERE nombre = 'Mesa';
2. Procedimientos y CALL
Cuando la idea no es devolver un valor, sino ejecutar una acción —un UPDATE—, la herramienta es un procedimiento, invocado con CALL, no con SELECT.
Escribe CREATE PROCEDURE actualizar_precio(p_id INT, p_nuevo_precio NUMERIC) LANGUAGE plpgsql AS $$ ... $$; cuyo cuerpo haga UPDATE productos SET precio = p_nuevo_precio WHERE id = p_id;. Después ejecutá CALL actualizar_precio(2, 55); y confirmá con SELECT nombre, precio FROM productos WHERE id = 2;
Si intentás SELECT actualizar_precio(2, 55); en vez de CALL, Postgres te va a devolver un error — un procedimiento no se puede invocar desde un SELECT.
Escribe reponer_stock(p_id INT, p_cantidad INT), que haga UPDATE productos SET stock = stock + p_cantidad WHERE id = p_id;. Ejecutá CALL reponer_stock(3, 20); (Lámpara, que estaba agotada) y confirmá su nuevo stock.
Escribe descontar_stock(p_id INT, p_cantidad INT) que valide que haya stock suficiente antes de descontarlo. Esta vez el cuerpo necesita una variable local para guardar el stock actual, con el bloque DECLARE:
DECLARE
v_stock_actual INT;
BEGIN
SELECT stock INTO v_stock_actual FROM productos WHERE id = p_id;
IF v_stock_actual < p_cantidad THEN
RAISE EXCEPTION 'Stock insuficiente';
END IF;
UPDATE productos SET stock = stock - p_cantidad WHERE id = p_id;
END;
SELECT ... INTO variable guarda el resultado de una consulta en una variable, en vez de devolverlo como una tabla. Pruébala: CALL descontar_stock(4, 3); (Estante, de 8 a 5) debería funcionar; CALL descontar_stock(4, 100); debería fallar con tu mensaje de error.
3. Triggers con OLD y NEW
El momento fuerte del taller: un código que se ejecuta solo, sin que nadie lo llame, apenas pasa algo en una tabla.
Crea una tabla nueva, auditoria_stock, con columnas id (clave primaria autoincremental), producto_id (entero), stock_anterior (entero), stock_nuevo (entero) y fecha (con DEFAULT now()).
Escribe registrar_cambio_stock() RETURNS TRIGGER, cuyo cuerpo haga INSERT INTO auditoria_stock (producto_id, stock_anterior, stock_nuevo) VALUES (OLD.id, OLD.stock, NEW.stock); y termine con RETURN NEW;. Todavía no la conectes a nada.
Conectá esa función a un evento real: CREATE TRIGGER trg_auditar_stock AFTER UPDATE ON productos FOR EACH ROW WHEN (OLD.stock IS DISTINCT FROM NEW.stock) EXECUTE FUNCTION registrar_cambio_stock();
Ejecutá UPDATE productos SET stock = 3 WHERE nombre = 'Mesa'; —sin mencionar la auditoría en ningún lado— y después, en la misma consulta, SELECT * FROM auditoria_stock;
Debería aparecer 1 fila en auditoria_stock, con stock_anterior = 15 y stock_nuevo = 3 — el trigger se disparó solo, apenas el UPDATE cambió el stock.
¿Qué pasa si actualizás el precio de Silla, sin tocar su stock? ¿Aparece una fila nueva en auditoria_stock? Escribí UPDATE productos SET precio = 999 WHERE nombre = 'Silla'; seguido de SELECT COUNT(*) FROM auditoria_stock; para comprobar tu predicción.
El WHEN (OLD.stock IS DISTINCT FROM NEW.stock) filtró este UPDATE: como el stock no cambió, registrar_cambio_stock() nunca se ejecutó, y auditoria_stock sigue con la única fila del ejercicio 3.4.
4. Trigger BEFORE de validación
Un trigger no solo puede reaccionar a lo que ya pasó (AFTER) — también puede impedir que algo inválido llegue a guardarse (BEFORE).
Escribe validar_precio_positivo() RETURNS TRIGGER, cuyo cuerpo haga IF NEW.precio <= 0 THEN RAISE EXCEPTION 'El precio debe ser mayor a 0'; END IF; y termine con RETURN NEW;. Conectala con CREATE TRIGGER trg_validar_precio BEFORE INSERT ON productos FOR EACH ROW EXECUTE FUNCTION validar_precio_positivo();
Intentá INSERT INTO productos (nombre, precio, stock) VALUES ('Silla rota', -10, 2); —debería fallar con tu mensaje de error—. Después, en el mismo editor, insertá uno válido: INSERT INTO productos (nombre, precio, stock) VALUES ('Cajonera', 60, 10);, y confirmá con SELECT COUNT(*) FROM productos;
El primer INSERT nunca llegó a guardarse —el trigger lo frenó antes—. Solo "Cajonera" quedó en la tabla, así que COUNT(*) debería mostrarte 6 (las 5 originales más esta).
Reto final: una regla que ningún procedimiento puede saltarse
Cierra el taller mostrando la ventaja real de un trigger sobre validar "a mano" dentro de cada procedimiento (como hiciste en el ejercicio 2.3).
- 1. Escribe
validar_stock_no_negativo() RETURNS TRIGGER, que hagaRAISE EXCEPTION 'El stock no puede quedar negativo';siNEW.stock < 0. - 2. Conectala con
CREATE TRIGGER trg_validar_stock BEFORE UPDATE ON productos FOR EACH ROW EXECUTE FUNCTION validar_stock_no_negativo(); - 3. Escribe un procedimiento nuevo,
vender_producto(p_id INT, p_cantidad INT), que simplemente hagaUPDATE productos SET stock = stock - p_cantidad WHERE id = p_id;—sin ninguna validación propia, a diferencia dedescontar_stock()del ejercicio 2.3. - 4. Probá
CALL vender_producto(4, 2);(debería funcionar) y despuésCALL vender_producto(4, 1000);(debería fallar).
vender_producto() no valida nada por sí mismo —y aun así, no puede dejar el stock en negativo. El trigger protege la tabla productos sin importar qué procedimiento o consulta intente modificarla, mientras que la validación de descontar_stock() en el ejercicio 2.3 solo protege a quien pase por ese procedimiento en particular. Si mañana escribís un tercer procedimiento que también toque el stock, tendrías que repetir esa validación a mano ahí también — a menos que la regla viva en un trigger, una sola vez.
Repaso
Preguntas cortas sobre lo que acabas de ejecutar en vivo. Tu progreso se guarda automáticamente en este navegador.
En el ejercicio 1.1, calcular_precio_final(precio) se usó dentro de un SELECT, junto a otras columnas. ¿Por qué esa herramienta tenía que ser una función, y no un procedimiento?
En el ejercicio 2.1, SELECT actualizar_precio(2, 55); habría funcionado exactamente igual que CALL actualizar_precio(2, 55);
En el ejercicio 2.3, ¿para qué sirvió específicamente el bloque DECLARE con v_stock_actual?
En el ejercicio 3.4, el UPDATE de Mesa no mencionó auditoria_stock ni registrar_cambio_stock() en ningún lado. ¿Por qué apareció una fila nueva en la auditoría de todos modos?
En el ejercicio 3.5, actualizar solo el precio de Silla no agregó ninguna fila a auditoria_stock. ¿Qué parte del trigger explica eso?
En el ejercicio 4.2, el INSERT con precio -10 nunca se guardó. ¿En qué momento actuó el trigger trg_validar_precio?
Clasifica cada elemento que escribiste en este taller según a qué categoría pertenece.
Sobre el reto final: vender_producto() no valida nada por sí mismo, y aun así no puede dejar el stock en negativo.
1. Eso pasa porque la validación vive en un , que protege la tabla sin importar qué procedimiento la modifique.
2. Si en cambio la validación estuviera solo dentro de descontar_stock(), un procedimiento nuevo que también tocara el stock para quedar protegido igual.
Une cada término con lo que hizo en este taller.
Cheat Sheet del taller
| Forma | Para qué sirve |
|---|---|
CREATE FUNCTION nombre(...) RETURNS tipo AS $$ ... $$ LANGUAGE plpgsql; | Guardar lógica que devuelve un valor, usable en un SELECT. |
CREATE PROCEDURE nombre(...) LANGUAGE plpgsql AS $$ ... $$; | Guardar una acción que no devuelve valor, invocada con CALL. |
CALL nombre(argumentos); | Ejecutar un procedimiento almacenado. |
DECLARE variable tipo; BEGIN ... END; | Declarar una variable local dentro de una función o procedimiento. |
SELECT columna INTO variable FROM tabla WHERE ...; | Guardar el resultado de una consulta en una variable, en vez de devolverlo como tabla. |
RAISE EXCEPTION 'mensaje'; | Detener la operación completa con un error. |
RETURNS TRIGGER | Marca que una función está pensada para un trigger, no para llamarse directo. |
OLD / NEW | La fila antes y después del cambio, dentro de la función de un trigger. |
CREATE TRIGGER nombre BEFORE|AFTER evento ON tabla FOR EACH ROW EXECUTE FUNCTION funcion(); | Conectar una función de trigger a un evento de una tabla. |
WHEN (condición) | Filtro extra: el trigger solo se dispara si la condición es verdadera. |
¿Sabías que...?
🗂️ Tu auditoria_stock es tuya sola
Cada estudiante que abre este taller corre su propia base de datos en su propia pestaña. Tu tabla auditoria_stock, con exactamente una fila tras el ejercicio 3.4, no existe en ningún otro navegador ni servidor — vive solo en la memoria de tu pestaña mientras esté abierta.
🔁 Un trigger puede quedar "colgado" sin querer
Si olvidás el WHEN de trg_auditar_stock, el trigger igual funciona — pero se dispara en cualquier UPDATE sobre productos, no solo cuando cambia el stock, insertando filas de auditoría de más. El WHEN no es opcional cuando la regla de negocio pide "solo si cambió esto".
🧩 DECLARE no es solo para triggers
El bloque DECLARE que usaste en descontar_stock() (un procedimiento, no un trigger) demuestra que las variables locales están disponibles en cualquier función o procedimiento de PL/pgSQL, no solo en la lógica de un trigger.