Migrar de pymssql a mssql-python

El controlador mssql-python es el controlador Python de primera mano de Microsoft para Microsoft SQL. Si prefieres una opción de controlador mantenida por Microsoft, ofrece:

  • No dependía de FreeTDS.
  • Múltiples cursores concurrentes por conexión.
  • Agrupación de conexiones integrada.
  • Soporte moderno para Python 3.10+.
  • Autenticación nativa de Microsoft Entra.
  • Objetos de fila con acceso por atributos de forma predeterminada.

Diferencias clave

Feature pymssql mssql-python
Estilo de parámetro format (%s, %d) qmark (?) y pyformat (%(name)s)
Biblioteca nativa FreeTDS DDBC (incluido en el paquete)
Agrupación de conexiones Externo Integrado
Cursores por conexión 1 Múltiple
Versión mínima de Python 3.6 3.10
callproc() Soportado No implementado
as_dict cursor Extension Objetos de fila (por defecto)
Copia masiva conn.bulk_copy() cursor.bulkcopy()
Confirmación automática predeterminada Off Off

Pasos básicos de migración

Los siguientes pasos repasan los cambios más comunes necesarios para migrar una aplicación pymssql a mssql-python.

1. Actualizar importaciones

Sustituye la pymssql importación por mssql_python:

Antes (pymssql):

import pymssql

Después de (mssql-python):

import mssql_python

2. Actualizar llamadas de conexión

PymsSQL utiliza argumentos posicionales. El controlador mssql-python utiliza una cadena de conexión o argumentos de palabras clave:

Antes (pymssql, argumentos posicionales):

conn = pymssql.connect("<server>", "<user>", "<password>", "<database>")

Antes (pymssql, argumentos por palabra clave):

conn = pymssql.connect(
    host=r"<server>\<instance>",
    user="<login>",
    password="<password>",
    database="<database>"
)

Después (mssql-python, cadena de conexión):

conn = mssql_python.connect(
    "Server=<server>;"
    "Database=<database>;"
    "UID=<username>;"
    "PWD=<password>;"
    "Encrypt=yes;"
)

Después (mssql-python, recomendado por Microsoft Entra):

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes;"
)

3. Actualizar marcadores de parámetros

pymssql utiliza los marcadores de formato %s y %d. El controlador mssql-python utiliza ? (qmark) o %(name)s (pyformat):

Antes (pymssql):

cursor.execute("SELECT * FROM Person.Person WHERE BusinessEntityID = %d AND FirstName = %s", (user_id, name))

Después (mssql-python, estilo qmark):

user_id, name = 1, "Ken"
cursor.execute("SELECT * FROM Person.Person WHERE BusinessEntityID = ? AND FirstName = ?", (user_id, name))
print(cursor.fetchone())

cursor.execute(
    "SELECT * FROM Person.Person WHERE BusinessEntityID = %(id)s AND FirstName = %(name)s",
    {"id": user_id, "name": name}
)
print(cursor.fetchone())

4. Actualizar executemany

Actualizar los marcadores de posición de SQL de %s/%d a ? o %(name)s

Antes (pymssql):

cursor.execute("CREATE TABLE #Persons (ID INT, Name NVARCHAR(50), Department NVARCHAR(50))")
cursor.executemany(
    "INSERT INTO #Persons VALUES (%d, %s, %s)",
    [(1, "John", "Sales"), (2, "Jane", "Marketing")]
)

Después (mssql-python, estilo qmark):

cursor.execute("IF OBJECT_ID('#Persons') IS NOT NULL DROP TABLE #Persons")
cursor.execute("CREATE TABLE #Persons (ID INT, Name NVARCHAR(50), Department NVARCHAR(50))")
cursor.executemany(
    "INSERT INTO #Persons VALUES (?, ?, ?)",
    [(1, "John", "Sales"), (2, "Jane", "Marketing")]
)
cursor.execute("SELECT * FROM #Persons")
for row in cursor:
    print(row)

5. Usar atributos de fila en lugar de as_dict cursores

pymssql requiere as_dict=True para acceder a las columnas por nombre. El controlador mssql-python devuelve Row objetos que admiten tanto el acceso a atributos como a índices por defecto:

Antes (pymssql):

cursor = conn.cursor(as_dict=True)
cursor.execute("SELECT BusinessEntityID, FirstName FROM Person.Person WHERE FirstName = %s", ("John",))
for row in cursor:
    print("ID=%d, Name=%s" % (row["BusinessEntityID"], row["FirstName"]))

Después (mssql-python, acceso a atributos por defecto):

cursor = conn.cursor()
cursor.execute("SELECT BusinessEntityID, FirstName FROM Person.Person WHERE FirstName = ?", ("John",))
for row in cursor:
    print(f"ID={row.BusinessEntityID}, Name={row.FirstName}")
    # Index access also works: row[0], row[1]

Migración de procedimientos almacenados

El controlador mssql-python no implementa callproc(). Utiliza EXECUTE sentencias en su lugar.

Usar EXECUTE para procedimientos almacenados

PymsSQL soporta callproc(), pero el controlador mssql-python no. Use EXECUTE en su lugar:

Antes (pymssql):

cursor.callproc("uspGetEmployeeManagers", (5,))
for row in cursor:
    print(row)

Después de (mssql-python):

cursor.execute("EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = ?", (5,))
for row in cursor:
    print(row)

Parámetros de salida

Utiliza variables T-SQL para capturar valores de salida en lugar de depender de callproc() parámetros de salida:

Antes (pymssql):

cursor.callproc("GetProductCount", (category_id,))
count = cursor.fetchval()

Después (mssql-python, variables de T-SQL):

cursor.execute("""
    DECLARE @count INT;
    SELECT @count = COUNT(*) FROM Production.Product
    WHERE ProductSubcategoryID = ?;
    SELECT @count AS ProductCount;
""", (1,))
product_count = cursor.fetchval()
print(f"Product count: {product_count}")

Migración masiva de copias

pymssql llama a bulk_copy() en la conexión. El controlador mssql-python llama a bulkcopy() en el cursor con más opciones:

Antes (pymssql):

conn.bulk_copy("##BulkDemo", [(1, 2)] * 1000)
conn.commit()

Después de (mssql-python):

cursor = conn.cursor()
cursor.execute("CREATE TABLE ##BulkDemo (Col1 INT, Col2 INT)")
conn.commit()
result = cursor.bulkcopy("##BulkDemo", [(1, 2)] * 1000)
print(f"Copied {result['rows_copied']} rows")
conn.commit()
cursor.execute("DROP TABLE ##BulkDemo")
conn.commit()

El método mssql-python bulkcopy() soporta batch_size, timeout, column_mappings, keep_identity, check_constraintstable_lock, keep_nulls, fire_triggers, y use_internal_transaction. Consulta la copia en bloque para obtener más información.

Varios cursores

PymsSQL solo permite un cursor activo por conexión. El controlador mssql-python soporta múltiples cursores concurrentes:

Antes (pymssql):

c1 = conn.cursor()
c1.execute("SELECT TOP 5 * FROM Person.Person")
c2 = conn.cursor()
c2.execute("SELECT TOP 5 * FROM Sales.SalesOrderHeader")
c1.fetchall()

Después de (mssql-python):

c1 = conn.cursor()
c1.execute("SELECT TOP 5 BusinessEntityID, FirstName FROM Person.Person")
persons = c1.fetchall()

c2 = conn.cursor()
c2.execute("SELECT TOP 5 SalesOrderID FROM Sales.SalesOrderHeader")
orders = c2.fetchall()
print(f"Persons: {len(persons)}, Orders: {len(orders)}")

Agrupación de conexiones

PymsSQL no tiene pooling incorporado. El controlador mssql-python lo incluye automáticamente:

Antes (pymssql, se requiere un pool externo):

from dbutils.pooled_db import PooledDB
pool = PooledDB(pymssql, host="server", user="user", password="pwd", database="db")
conn = pool.connection()

Después de (mssql-python, el pooling es automático):

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

Gestión de errores

El controlador mssql-python utiliza la misma jerarquía de excepciones que pymssql, por lo que la mayoría de los gestores de excepciones solo requieren un cambio de nombre de módulo:

Antes (pymssql):

try:
    cursor.execute(query)
except pymssql.OperationalError as e:
    print(f"Operation failed: {e}")
except pymssql.InterfaceError as e:
    print(f"Interface error: {e}")

Después de (mssql-python):

try:
    cursor = conn.cursor()
    cursor.execute("SELECT TOP 1 * FROM Production.Product")
    row = cursor.fetchone()
    print(row)
except mssql_python.OperationalError as e:
    print(f"Operation failed: {e}")
except mssql_python.InterfaceError as e:
    print(f"Interface error: {e}")

Ejemplo de migración completa

Lo siguiente muestra la misma función escrita con pymssql y luego reescrita con mssql-python.

Antes (pymssql)

Esta versión utiliza argumentos de conexión posicional, as_dict=True, y %d marcadores de parámetros:

import pymssql

def get_orders(customer_id: int):
    conn = pymssql.connect("<server>", "<user>", "<password>", "<database>")
    cursor = conn.cursor(as_dict=True)

    cursor.execute("""
        SELECT TOP 10 SalesOrderID, OrderDate, TotalDue
        FROM Sales.SalesOrderHeader
        WHERE CustomerID = %d
        ORDER BY OrderDate DESC
    """, (customer_id,))

    orders = []
    for row in cursor:
        orders.append({
            "id": row["SalesOrderID"],
            "date": row["OrderDate"],
            "total": row["TotalDue"]
        })

    cursor.close()
    conn.close()
    return orders

Después (mssql-python)

Los cambios estructurales clave son marcadores de parámetros, estilo de conexión y acceso a filas:

import mssql_python

def get_orders(customer_id: int):
    conn = mssql_python.connect(
        "Server=<server>;"
        "Database=<database>;"
        "UID=<username>;"
        "PWD=<password>;"
    )
    cursor = conn.cursor()

    cursor.execute("""
        SELECT TOP 10 SalesOrderID, OrderDate, TotalDue
        FROM Sales.SalesOrderHeader
        WHERE CustomerID = ?
        ORDER BY OrderDate DESC
    """, (customer_id,))

    orders = []
    for row in cursor:
        orders.append({
            "id": row.SalesOrderID,
            "date": row.OrderDate,
            "total": row.TotalDue
        })

    cursor.close()
    conn.close()
    return orders

Los cambios estructurales son:

  1. import pymssqlimport mssql_python.
  2. Argumentos posicionales de conexión → cadena de conexión con parámetros con nombre.
  3. %d marcador de parámetros → ?.
  4. cursor(as_dict=True)cursor() con acceso a atributos (row.SalesOrderID en lugar de row["SalesOrderID"]).

Checklist

  • [ ] Actualizar importaciones desde pymssql a mssql_python.
  • [ ] Convertir llamadas de conexión de argumentos posicionales a cadenas de conexión.
  • [ ] Convertir %s/%d marcadores de parámetros a ? o .%(name)s
  • [ ] Usa EXECUTE sentencias para llamadas a procedimientos almacenados.
  • [ ] Utiliza el acceso a atributos Row en lugar de los cursores as_dict=True.
  • [ ] Migrar conn.bulk_copy() a cursor.bulkcopy().
  • [ ] Eliminar la configuración de la agrupación externa de conexiones.
  • [ ] Eliminar FreeTDS de los requisitos de despliegue.
  • [ ] Actualizar los nombres de las clases de manejo de excepciones.
  • [ ] Prueba todas las consultas y procedimientos almacenados.