Llamar a procedimientos almacenados con mssql-python

El controlador mssql-python no implementa el callproc() método de la especificación DB-API 2.0. Llamar a callproc() genera NotSupportedError. En su lugar, utiliza la secuencia de escape ODBC {CALL ...} con métodos estándar de ejecución de consultas.

Ejecución básica de procedimientos almacenados

Sin parámetros

Ejecuta un procedimiento almacenado mediante la secuencia de escape {CALL}. Este ejemplo llama al sp_databases procedimiento almacenado del sistema, que no toma parámetros y devuelve una fila por base de datos:

import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("{CALL sp_databases}")

for row in cursor:
    print(row.DATABASE_NAME, row.DATABASE_SIZE)

Con parámetros de entrada

Pasar parámetros usando marcadores posicionales o con nombre:

# Single parameter
cursor.execute(
    "{CALL dbo.uspGetManagerEmployees(?)}", (16,)
)

for row in cursor:
    print(row.FirstName, row.LastName)

Parámetros de salida

Declarar y recuperar parámetros de salida

Los procedimientos almacenados de SQL Server pueden devolver valores a través de parámetros de salida. Utiliza variables Transact-SQL (T-SQL) para capturar los valores de salida y luego recupérelos mediante una instrucción SELECT:

cursor.execute("""
    DECLARE @total_out MONEY;
    SELECT @total_out = SUM(TotalDue)
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = %(customer_id)s;
    SELECT @total_out AS TotalAmount;
""", {"customer_id": 29825})

row = cursor.fetchone()
total = row.TotalAmount
print(f"Customer total: ${total}")

Múltiples parámetros de salida

Captura múltiples valores de salida declarando variables separadas. El mismo patrón funciona con cualquier procedimiento almacenado que tenga OUTPUT parámetros:

cursor.execute("""
    DECLARE @total_orders INT, @total_spent MONEY;
    SELECT @total_orders = COUNT(*), @total_spent = SUM(TotalDue)
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = %(cust_id)s;
    SELECT @total_orders AS OrderCount, @total_spent AS TotalSpent;
""", {"cust_id": 29825})

stats = cursor.fetchone()
print(f"Orders: {stats.OrderCount}, Total spent: ${stats.TotalSpent}")

Valores devueltos

Captura el valor de retorno del procedimiento almacenado

Ejecuta un procedimiento almacenado que devuelva resultados:

cursor.execute("EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(emp_id)s", {"emp_id": 5})

rows = cursor.fetchall()
if rows:
    print(f"Found {len(rows)} managers in chain")
    for row in rows:
        print(f"  Manager: {row.FirstName} {row.LastName}")
else:
    print("No managers found")

Valor de retorno con parámetros de salida

cursor.execute("""
    DECLARE @return_value INT, @message NVARCHAR(500);
    SELECT @return_value = CASE WHEN COUNT(*) > 0 THEN 0 ELSE 1 END,
           @message = CASE WHEN COUNT(*) > 0 THEN N'Customer found' ELSE N'Customer not found' END
    FROM Sales.Customer WHERE CustomerID = %(cust_id)s;
    SELECT @return_value AS ReturnCode, @message AS Message;
""", {"cust_id": 29825})

result = cursor.fetchone()
print(f"Return code: {result.ReturnCode}, Message: {result.Message}")

Conjuntos de resultados

Conjunto de resultados único

Ejecuta un procedimiento almacenado que devuelva un único conjunto de resultados:

cursor.execute(
    "{CALL dbo.uspGetBillOfMaterials(?, ?)}",
    (800, "2026-01-01")
)

customers = cursor.fetchall()
for row in customers:
    print(f"{row.ProductAssemblyID}: {row.ComponentDesc}")

Varios conjuntos de resultados

Algunos procedimientos almacenados devuelven varios conjuntos de resultados. Use nextset() para navegar entre ellos:

cursor.execute("""
    SELECT TOP 1 SalesOrderID, OrderDate, TotalDue
    FROM Sales.SalesOrderHeader WHERE CustomerID = 29825;
    SELECT TOP 3 Name, ListPrice
    FROM Production.Product WHERE ListPrice > 0
    ORDER BY ListPrice DESC;
""")

# First result set: order header
order = cursor.fetchone()
print(f"Order: {order.SalesOrderID}, Date: {order.OrderDate}")

cursor.nextset()

# Second result set: products
print("Products:")
for item in cursor:
    print(f"  {item.Name}: ${item.ListPrice}")

Consulta si hay más conjuntos de resultados

Recorrer todos los conjuntos de resultados devueltos por un procedimiento almacenado mediante nextset():

cursor.execute("SELECT TOP 3 ProductID, Name FROM Production.Product; SELECT TOP 3 FirstName, LastName FROM Person.Person")

result_set_num = 1
while True:
    print(f"--- Result Set {result_set_num} ---")
    for row in cursor:
        print(row)
    
    if not cursor.nextset():
        break
    result_set_num += 1

Transacciones con procedimientos almacenados

Control explícito de transacciones

Encapsule varias llamadas a procedimientos almacenados en una transacción para garantizar la atomicidad:

conn.autocommit = False

try:
    cursor.execute("{CALL dbo.DebitAccount(?, ?)}", (1001, 100.00))
    
    cursor.execute("{CALL dbo.CreditAccount(?, ?)}", (1002, 100.00))
    
    conn.commit()
    print("Transfer completed")
except mssql_python.DatabaseError as e:
    conn.rollback()
    print(f"Transfer failed: {e}")

Deja que el procedimiento almacenado gestione la transacción

Si el procedimiento almacenado gestiona sus propias transacciones:

conn.autocommit = True  # Let SP manage transactions

cursor.execute("""
    DECLARE @result INT;
    EXECUTE @result = dbo.TransferFunds 
        @FromAccount = %(from_acc)s,
        @ToAccount = %(to_acc)s,
        @Amount = %(amount)s;
    SELECT @result AS TransferResult;
""", {"from_acc": 1001, "to_acc": 1002, "amount": 100.00})

result = cursor.fetchone()
if result.TransferResult == 0:
    print("Transfer successful")

Gestión de errores

Detectar errores en procedimientos almacenados

Gestiona las excepciones generadas por procedimientos almacenados o sentencias Transact-SQL:

try:
    cursor.execute("{CALL dbo.DangerousProcedure}")
except mssql_python.ProgrammingError as e:
    # Handle SQL errors raised by RAISERROR or THROW
    print(f"Stored procedure error: {e}")
except mssql_python.DatabaseError as e:
    # Handle other database errors
    print(f"Database error: {e}")

Capturar instrucciones PRINT y mensajes informativos

Las instrucciones de SQL Server PRINT y RAISERROR con gravedad inferior a 11 se capturan en cursor.messages tras su ejecución. Cada entrada es de tipo tupla (message_type, message_text). Cuando PRINT se ejecuta antes de un conjunto de resultados, ocupa un conjunto de resultados propio, sin filas, así que lee primero cursor.messages y luego llama a nextset() para acceder a las filas:

cursor.execute("PRINT 'Operation complete'; SELECT 1 AS Status")

for msg_type, msg_text in cursor.messages:
    print(f"Server message: {msg_text}")

cursor.nextset()
row = cursor.fetchone()
print(f"Status: {row.Status}")

Cuando un procedimiento almacenado emite PRINT mensajes a través de múltiples conjuntos de resultados, lee cursor.messages después execute() y de nuevo después de cada nextset() llamada para que se capturen mensajes de cada conjunto de resultados:

cursor.execute("""
    PRINT 'Starting first result set';
    SELECT TOP 3 ProductID, Name FROM Production.Product;
    PRINT 'Starting second result set';
    SELECT TOP 3 FirstName, LastName FROM Person.Person;
""")

all_messages = []
while True:
    all_messages.extend(cursor.messages)
    if cursor.description:
        for row in cursor:
            print(row)
    if not cursor.nextset():
        break

for _, text in all_messages:
    print(f"Server: {text}")

Para la API completa cursor.messages , véase Gestión de cursores.

procedimientos recomendados

Uso de parámetros nombrados

Los parámetros nombrados son más claros y mantienen la independencia del orden:

# Recommended: {CALL} with positional parameters
cursor.execute(
    "{CALL dbo.uspGetBillOfMaterials(?, ?)}",
    (800, "2026-01-01")
)

# Also valid: EXECUTE with named T-SQL parameters
cursor.execute("""
    EXECUTE dbo.uspGetBillOfMaterials
        @StartProductID = ?,
        @CheckDate = ?
""", (800, "2026-01-01"))

Manejar parámetros de salida anulables

Comprueba los valores NULL al recuperar los parámetros de salida de procedimientos almacenados:

cursor.execute("""
    DECLARE @optional_value NVARCHAR(100);
    SELECT @optional_value = Color FROM Production.Product WHERE ProductID = %(id)s;
    SELECT @optional_value AS OutputValue;
""", {"id": 1})

result = cursor.fetchone()
if result.OutputValue is not None:
    print(f"Value: {result.OutputValue}")
else:
    print("No value returned")

Uso SET NOCOUNT de ON en procedimientos almacenados

Para un mejor rendimiento y un manejo de resultados más limpio, asegúrate de que tus procedimientos almacenados incluyan:

CREATE PROCEDURE dbo.MyProcedure
AS
BEGIN
    SET NOCOUNT ON;  -- Prevents "n rows affected" messages
    -- procedure logic
END

Ejemplo: flujo de trabajo completo

Aquí tienes un ejemplo práctico que llama a un procedimiento almacenado, recupera valores de salida y gestiona errores:

import mssql_python

def get_employee_report(manager_id: int) -> dict:
    """Look up a manager's employees and compute average vacation hours."""
    conn = mssql_python.connect(connection_string)
    conn.autocommit = False
    cursor = conn.cursor()

    try:
        # Get manager info
        cursor.execute("""
            SELECT BusinessEntityID, JobTitle
            FROM HumanResources.Employee
            WHERE BusinessEntityID = %(mgr)s
        """, {"mgr": manager_id})
        mgr = cursor.fetchone()
        print(f"Manager {mgr.BusinessEntityID}: {mgr.JobTitle}")

        # Get direct reports via stored procedure
        cursor.execute("{CALL dbo.uspGetManagerEmployees(?)}", (manager_id,))
        employees = cursor.fetchall()
        print(f"Found {len(employees)} employee(s)")

        # Compute average vacation hours
        cursor.execute("""
            DECLARE @avg_hours INT;
            SELECT @avg_hours = AVG(VacationHours)
            FROM HumanResources.Employee;
            SELECT @avg_hours AS AvgVacation;
        """)
        avg = cursor.fetchone().AvgVacation
        print(f"Avg vacation hours: {avg}")

        conn.commit()
        return {"manager": mgr.JobTitle, "reports": len(employees), "avg_vacation": avg}

    except mssql_python.DatabaseError as e:
        conn.rollback()
        raise
    finally:
        cursor.close()
        conn.close()

# Usage
result = get_employee_report(manager_id=16)

Consigue claves generadas usando OUTPUT INSERTED

Para recuperar un valor identidad de un INSERT (con o sin procedimiento almacenado), use OUTPUT INSERTED en lugar de SCOPE_IDENTITY(). Este enfoque devuelve el valor en el mismo conjunto de resultados:

cursor.execute("""
    INSERT INTO Production.ProductCategory (Name)
    OUTPUT INSERTED.ProductCategoryID
    VALUES (%(name)s)
""", {"name": "Custom Parts"})

new_id = cursor.fetchval()
print(f"New category ID: {new_id}")

Este patrón funciona para cualquier tabla con columna identidad y no requiere un procedimiento almacenado.

Características no admitidas

callproc()

El controlador mssql-python genera NotSupportedError si llama a cursor.callproc(). Úsate cursor.execute("{CALL ...}") o cursor.execute("EXECUTE ...") , en su lugar, como se muestra a lo largo de este artículo.

Parámetros con valores de tabla (TVPs)

El mssql-python controlador no soporta parámetros con valores de tabla. Para ver la matriz completa de soporte de funciones, véase ciclo de vida de soporte. Si necesitas pasar un conjunto de filas a un procedimiento almacenado, utiliza estas alternativas:

  • Inserta primero en una tabla temporal y luego haz que el procedimiento almacenado sea leído de ella.
  • Usa bulkcopy() para cargar datos en una tabla de ensayo.
  • Pasa una cadena JSON y analízala con OPENJSON dentro del procedimiento.