Title: Actividad 12: Manejo de errores y excepciones (práctica con PL/pgSQL)
Course: 2025 - LISI - Base de Datos Avanzadas 3-3 > Section: Unidad 1 - Comandos avanzados en RDB

Objetivo

Implementar bloques BEGIN…EXCEPTION…END en PostgreSQL para capturar errores frecuentes (duplicados, llaves foráneas, datos inválidos), registrarlos en una bitácora y responder de forma controlada en procedimientos/funciones que operan sobre la base escuela.

Instrucciones

1. Identificación (agregar como comentario al inicio del script)

-- Alumno: [Tu nombre completo] | Profesor: [Nombre del profesor]
-- Actividad 12: Manejo de errores y excepciones (Práctica)


1. Preparación (garantizar integridad y bitácora)
Crea una tabla de errores y asegúrate de que la relación alumno–materia no se duplique.

-- 2.1 Bitácora de errores
CREATE TABLE IF NOT EXISTS errores (
  id           BIGSERIAL PRIMARY KEY,
  ocurrio_en   TIMESTAMP NOT NULL DEFAULT now(),
  rutina       TEXT NOT NULL,        -- nombre de la función/procedimiento
  codigo_sql   TEXT NOT NULL,        -- SQLSTATE
  mensaje      TEXT NOT NULL,        -- mensaje resumido
  detalle      TEXT                  -- detalle opcional
);

-- 2.2 Restricción única para evitar inscripciones duplicadas
-- (ajusta los nombres de columnas según tu modelo)
ALTER TABLE IF EXISTS alumno_materia
ADD CONSTRAINT IF NOT EXISTS uq_alumno_materia UNIQUE (id_alumno, id_materia);


1. Procedimiento de utilidad para registrar errores
Centraliza el registro de errores (lo podrás reutilizar en otras rutinas).

-- PostgreSQL 11+ (soporta CREATE PROCEDURE / CALL)
CREATE OR REPLACE PROCEDURE registrar_error(
  p_rutina  TEXT,
  p_codigo  TEXT,
  p_mensaje TEXT,
  p_detalle TEXT DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO errores (rutina, codigo_sql, mensaje, detalle)
  VALUES (p_rutina, p_codigo, p_mensaje, p_detalle);
END;
$$;


1. Función “segura” para inscribir alumnos a materias
Debe manejar errores típicos:

- foreign_key_violation: si el id_alumno o id_materia no existen.
- unique_violation: si ya está inscrito.
- Otros: cualquier error no previsto.

CREATE OR REPLACE FUNCTION inscribir_alumno_seguro(p_id_alumno INT, p_id_materia INT)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
  v_msg TEXT;
BEGIN
  -- Intento principal
  INSERT INTO alumno_materia (id_alumno, id_materia)
  VALUES (p_id_alumno, p_id_materia);

  v_msg := format('Inscripción exitosa: alumno=%s en materia=%s', p_id_alumno, p_id_materia);
  RAISE NOTICE '%', v_msg;
  RETURN v_msg;

EXCEPTION
  WHEN foreign_key_violation THEN
    PERFORM registrar_error(
      'inscribir_alumno_seguro',
      SQLSTATE,
      'Violación de llave foránea (alumno o materia no existen)',
      format('alumno=%s, materia=%s', p_id_alumno, p_id_materia)
    );
    RETURN 'Error: el alumno o la materia no existen (FK)';

  WHEN unique_violation THEN
    PERFORM registrar_error(
      'inscribir_alumno_seguro',
      SQLSTATE,
      'Inscripción duplicada detectada',
      format('alumno=%s, materia=%s', p_id_alumno, p_id_materia)
    );
    RETURN 'Aviso: el alumno ya estaba inscrito en esa materia';

  WHEN others THEN
    PERFORM registrar_error(
      'inscribir_alumno_seguro',
      SQLSTATE,
      COALESCE(SQLERRM, 'Error no especificado'),
      format('alumno=%s, materia=%s', p_id_alumno, p_id_materia)
    );
    RETURN 'Error general durante la inscripción';
END;
$$;


1. Pruebas guiadas (ejecutar y observar resultados)

Ajusta IDs existentes en tu base escuela.

- Caso A (exitoso):

SELECT inscribir_alumno_seguro( /* id_alumno_existe */ 1, /* id_materia_existe */ 1 );


- Caso B (FK inválida): usar un id_alumno o id_materia que no exista

SELECT inscribir_alumno_seguro(99999, 1);  -- alumno inexistente


- Caso C (duplicado): repetir una inscripción válida para provocar unique_violation

-- Si ya quedó inscrito en Caso A, vuelve a ejecutar el mismo par:
SELECT inscribir_alumno_seguro(1, 1);


- Revisión de la bitácora:

SELECT * FROM errores ORDER BY id DESC;


Esperado: se registran filas en errores para los casos B y C (y para cualquier otro error). El caso A no debe registrar error.

1. (Opcional) Procedimiento transaccional con protección
Ejemplo de rutina que intenta inscribir a varios alumnos a la vez y no se cae ante fallos individuales (registra y sigue):

CREATE OR REPLACE PROCEDURE inscribir_lote_seguro(p_id_materia INT, p_ids_alumnos INT[])
LANGUAGE plpgsql
AS $$
DECLARE
  i INT;
  r TEXT;
BEGIN
  FOREACH i IN ARRAY p_ids_alumnos
  LOOP
    BEGIN
      r := inscribir_alumno_seguro(i, p_id_materia);
      RAISE NOTICE 'Alumno %: %', i, r;
    EXCEPTION WHEN others THEN
      -- Blindaje adicional: nunca detener el lote
      PERFORM registrar_error(
        'inscribir_lote_seguro',
        SQLSTATE,
        COALESCE(SQLERRM, 'Error en lote'),
        format('alumno=%s, materia=%s', i, p_id_materia)
      );
      RAISE NOTICE 'Alumno %: error registrado y continuado', i;
    END;
  END LOOP;
END;
$$;


Prueba:

CALL inscribir_lote_seguro(1, ARRAY[1, 2, 99999]);  -- incluye un id inválido para ver el manejo


Evidencia a entregar (en PDF)

- Script con:
  - Creación de errores.
  - registrar_error.
  - inscribir_alumno_seguro (y inscribir_lote_seguro si haces el opcional).
  - La restricción UNIQUE en alumno_materia.
- Capturas de:
  - Ejecuciones de los Casos A, B, C (y del lote opcional).
  - Consulta a errores mostrando los registros creados.
- Breve comentario (4–6 líneas):
  - ¿Qué excepción se presentó en cada caso?
  - ¿Qué mensaje devolviste al usuario y qué quedó registrado en tabla?

Tips para clase

- Fomenta que no oculten los errores graves: registrar ✔️, y devolver un mensaje útil.
- Usa SQLSTATE para distinguir tipos de error (p. ej., 23503 FK, 23505 UNIQUE).
- Este patrón (función segura + bitácora) se reutiliza en altas, bajas y cambios.

Objetivo

Implementar bloques BEGIN…EXCEPTION…END en PostgreSQL para capturar errores frecuentes (duplicados, llaves foráneas, datos inválidos), registrarlos en una bitácora y responder de forma controlada en procedimientos/funciones que operan sobre la base escuela.

Instrucciones

1. Identificación (agregar como comentario al inicio del script)

-- Alumno: [Tu nombre completo] | Profesor: [Nombre del profesor]
-- Actividad 12: Manejo de errores y excepciones (Práctica)


1. Preparación (garantizar integridad y bitácora)
Crea una tabla de errores y asegúrate de que la relación alumno–materia no se duplique.

-- 2.1 Bitácora de errores
CREATE TABLE IF NOT EXISTS errores (
  id           BIGSERIAL PRIMARY KEY,
  ocurrio_en   TIMESTAMP NOT NULL DEFAULT now(),
  rutina       TEXT NOT NULL,        -- nombre de la función/procedimiento
  codigo_sql   TEXT NOT NULL,        -- SQLSTATE
  mensaje      TEXT NOT NULL,        -- mensaje resumido
  detalle      TEXT                  -- detalle opcional
);

-- 2.2 Restricción única para evitar inscripciones duplicadas
-- (ajusta los nombres de columnas según tu modelo)
ALTER TABLE IF EXISTS alumno_materia
ADD CONSTRAINT IF NOT EXISTS uq_alumno_materia UNIQUE (id_alumno, id_materia);


1. Procedimiento de utilidad para registrar errores
Centraliza el registro de errores (lo podrás reutilizar en otras rutinas).

-- PostgreSQL 11+ (soporta CREATE PROCEDURE / CALL)
CREATE OR REPLACE PROCEDURE registrar_error(
  p_rutina  TEXT,
  p_codigo  TEXT,
  p_mensaje TEXT,
  p_detalle TEXT DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO errores (rutina, codigo_sql, mensaje, detalle)
  VALUES (p_rutina, p_codigo, p_mensaje, p_detalle);
END;
$$;


1. Función “segura” para inscribir alumnos a materias
Debe manejar errores típicos:

- foreign_key_violation: si el id_alumno o id_materia no existen.
- unique_violation: si ya está inscrito.
- Otros: cualquier error no previsto.

CREATE OR REPLACE FUNCTION inscribir_alumno_seguro(p_id_alumno INT, p_id_materia INT)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
  v_msg TEXT;
BEGIN
  -- Intento principal
  INSERT INTO alumno_materia (id_alumno, id_materia)
  VALUES (p_id_alumno, p_id_materia);

  v_msg := format('Inscripción exitosa: alumno=%s en materia=%s', p_id_alumno, p_id_materia);
  RAISE NOTICE '%', v_msg;
  RETURN v_msg;

EXCEPTION
  WHEN foreign_key_violation THEN
    PERFORM registrar_error(
      'inscribir_alumno_seguro',
      SQLSTATE,
      'Violación de llave foránea (alumno o materia no existen)',
      format('alumno=%s, materia=%s', p_id_alumno, p_id_materia)
    );
    RETURN 'Error: el alumno o la materia no existen (FK)';

  WHEN unique_violation THEN
    PERFORM registrar_error(
      'inscribir_alumno_seguro',
      SQLSTATE,
      'Inscripción duplicada detectada',
      format('alumno=%s, materia=%s', p_id_alumno, p_id_materia)
    );
    RETURN 'Aviso: el alumno ya estaba inscrito en esa materia';

  WHEN others THEN
    PERFORM registrar_error(
      'inscribir_alumno_seguro',
      SQLSTATE,
      COALESCE(SQLERRM, 'Error no especificado'),
      format('alumno=%s, materia=%s', p_id_alumno, p_id_materia)
    );
    RETURN 'Error general durante la inscripción';
END;
$$;


1. Pruebas guiadas (ejecutar y observar resultados)

Ajusta IDs existentes en tu base escuela.

- Caso A (exitoso):

SELECT inscribir_alumno_seguro( /* id_alumno_existe */ 1, /* id_materia_existe */ 1 );


- Caso B (FK inválida): usar un id_alumno o id_materia que no exista

SELECT inscribir_alumno_seguro(99999, 1);  -- alumno inexistente


- Caso C (duplicado): repetir una inscripción válida para provocar unique_violation

-- Si ya quedó inscrito en Caso A, vuelve a ejecutar el mismo par:
SELECT inscribir_alumno_seguro(1, 1);


- Revisión de la bitácora:

SELECT * FROM errores ORDER BY id DESC;


Esperado: se registran filas en errores para los casos B y C (y para cualquier otro error). El caso A no debe registrar error.

1. (Opcional) Procedimiento transaccional con protección
Ejemplo de rutina que intenta inscribir a varios alumnos a la vez y no se cae ante fallos individuales (registra y sigue):

CREATE OR REPLACE PROCEDURE inscribir_lote_seguro(p_id_materia INT, p_ids_alumnos INT[])
LANGUAGE plpgsql
AS $$
DECLARE
  i INT;
  r TEXT;
BEGIN
  FOREACH i IN ARRAY p_ids_alumnos
  LOOP
    BEGIN
      r := inscribir_alumno_seguro(i, p_id_materia);
      RAISE NOTICE 'Alumno %: %', i, r;
    EXCEPTION WHEN others THEN
      -- Blindaje adicional: nunca detener el lote
      PERFORM registrar_error(
        'inscribir_lote_seguro',
        SQLSTATE,
        COALESCE(SQLERRM, 'Error en lote'),
        format('alumno=%s, materia=%s', i, p_id_materia)
      );
      RAISE NOTICE 'Alumno %: error registrado y continuado', i;
    END;
  END LOOP;
END;
$$;


Prueba:

CALL inscribir_lote_seguro(1, ARRAY[1, 2, 99999]);  -- incluye un id inválido para ver el manejo


Evidencia a entregar (en PDF)

- Script con:
  - Creación de errores.
  - registrar_error.
  - inscribir_alumno_seguro (y inscribir_lote_seguro si haces el opcional).
  - La restricción UNIQUE en alumno_materia.
- Capturas de:
  - Ejecuciones de los Casos A, B, C (y del lote opcional).
  - Consulta a errores mostrando los registros creados.
- Breve comentario (4–6 líneas):
  - ¿Qué excepción se presentó en cada caso?
  - ¿Qué mensaje devolviste al usuario y qué quedó registrado en tabla?

Tips para clase

- Fomenta que no oculten los errores graves: registrar ✔️, y devolver un mensaje útil.
- Usa SQLSTATE para distinguir tipos de error (p. ej., 23503 FK, 23505 UNIQUE).
- Este patrón (función segura + bitácora) se reutiliza en altas, bajas y cambios.