Saltar al contenido

# sql-desde-python

SQL desde Python

Volver a la portada del curso
Lectura (Markdown)

Gestión de Errores en SQL

Sección 4 · Interacción Avanzada con Bases de Datos

Gestión de errores en SQL

Un programa que trabaja con una base de datos va a encontrar errores: un correo que ya existe, un precio negativo, un pedido de un cliente que no está registrado, una tabla mal escrita. La pregunta no es cómo evitar que ocurran, sino qué hacer cuando ocurren: deshacer lo que quedó a medias, avisar algo útil y no esconder el problema. Esta lectura recorre las excepciones de sqlite3, las restricciones que las provocan y los patrones para manejarlas. El notebook transacciones-y-errores provoca cada caso sobre la base de PyShop.

1. La jerarquía de excepciones

sqlite3 sigue la especificación DB-API 2.0 (PEP 249), así que sus excepciones tienen los mismos nombres que en otras bibliotecas de bases de datos de Python. Todas heredan de sqlite3.Error; las que vienen de la base heredan además de sqlite3.DatabaseError:

  • IntegrityError: los datos violan una restricción (UNIQUE, CHECK, NOT NULL, FOREIGN KEY). El SQL es correcto; el problema son los valores.
  • OperationalError: algo impide ejecutar la operación: una tabla que no existe, SQL mal escrito, una base bloqueada por otra conexión o un archivo que no se puede abrir.
  • ProgrammingError: uso incorrecto de la API, como pasar más parámetros que marcadores o usar una conexión ya cerrada.

Existen otras (DataError, InternalError, NotSupportedError, InterfaceError), menos frecuentes. La cadena de herencia de IntegrityError es IntegrityError → DatabaseError → Error → Exception: capturar sqlite3.Error atrapa cualquiera de ellas.

2. Cada restricción, su error

El esquema de PyShop declara restricciones en cada tabla. El notebook intenta violarlas una por una; estos son los mensajes reales:

IntentoExcepción y mensaje
Cliente con un correo ya registradoIntegrityError: UNIQUE constraint failed: clientes.correo
Producto con precio -5IntegrityError: CHECK constraint failed: precio > 0
Pedido del cliente 9999IntegrityError: FOREIGN KEY constraint failed
Producto sin nombreIntegrityError: NOT NULL constraint failed: productos.nombre
SELECT * FROM productoOperationalError: no such table: producto
SELEC * FROM productosOperationalError: near "SELEC": syntax error
Un ? y dos valoresProgrammingError: Incorrect number of bindings supplied

Un caso merece atención: las claves foráneas. SQLite no revisa REFERENCES salvo que la conexión ejecute PRAGMA foreign_keys = ON, y esa configuración vale solo para esa conexión. Sin el PRAGMA, el pedido del cliente 9999 se inserta sin error y queda huérfano. Por eso conviene activarlo en la función que abre la conexión, una sola vez.

3. Errores y transacciones

Cuando una sentencia falla dentro de una transacción, las sentencias anteriores de esa transacción siguen pendientes. Si el código atrapa el error y sigue como si nada, el siguiente commit() guarda un estado a medias. La regla: si algo falla, deshaz todo. with con: lo hace solo: confirma si el bloque termina bien y llama a rollback() si sale una excepción, que después se vuelve a lanzar.

def registrar_cliente(con, nombre, correo, ciudad):
    try:
        with con:
            cur = con.execute(
                "INSERT INTO clientes (nombre, correo, ciudad, alta) "
                "VALUES (?, ?, ?, '2025-10-07')",
                (nombre, correo, ciudad))
    except sqlite3.IntegrityError as e:
        if "UNIQUE" in str(e):
            return False, f"Ya existe un cliente con el correo {correo}."
        return False, "Faltan datos obligatorios del cliente."
    else:
        return True, cur.lastrowid

La función atrapa solo IntegrityError, el error que sabe explicar, y lo traduce a un mensaje para el usuario. Un OperationalError (por ejemplo, la tabla no existe) no se atrapa: sube, porque indica un fallo del programa y no de los datos. El bloque else corre solo si no hubo error; finally, si lo usas, corre siempre y es el lugar para cerrar recursos.

4. Excepciones propias

Un mensaje como «CHECK constraint failed: stock >= 0» le sirve al programador, no a quien hace el pedido. Una excepción propia lleva información de negocio:

class StockInsuficiente(Exception):
    pass

fila = con.execute(
    "SELECT nombre, stock FROM productos WHERE id = ?", (15,)
).fetchone()
if fila[1] < 10:
    raise StockInsuficiente(f"{fila[0]}: pides 10, hay {fila[1]}")

Validar antes de escribir permite un mensaje claro («Juego de cuchillos: pides 10, hay 9»), y el CHECK de la tabla queda como última red de seguridad. Al traducir un error, usa raise MiError(...) from e para conservar el original en el rastreo.

Trampas comunes

  • except Exception: pass. Esconde el error y puede dejar una transacción abierta con cambios a medias que el siguiente commit guardará.
  • Capturar demasiado. Atrapar sqlite3.Error para mostrar «correo repetido» también disfraza una tabla mal escrita.
  • Olvidar PRAGMA foreign_keys = ON. Sin él, SQLite acepta referencias a filas que no existen.
  • Mostrar el mensaje técnico al usuario. Regístralo para depurar; muestra uno que explique qué corregir.
  • Decidir por el texto del mensaje sin necesidad. Úsalo solo para distinguir restricciones; el tipo de excepción es la primera señal.

En el notebook, provoca cada error de la tabla y escribe clase_de_error(con, sql), que devuelve el nombre de la excepción sin dejar cambios pendientes. Es la mejor forma de aprender a reconocerlas.

Recursos