Migrar de SQLite a Microsoft SQL con mssql-python

SQLite es la base de datos predeterminada para muchos proyectos de Python, FastAPI y las aplicaciones Flask. Te permite construir tu aplicación rápidamente retrasando la decisión de la plataforma de datos hasta más adelante. Cuando llegue el momento de llevar tu aplicación a producción, necesitas soporte para usuarios concurrentes, seguridad basada en roles, alta disponibilidad, recuperación ante desastres y otras funciones empresariales. Necesitas migrar a Microsoft SQL usando el controlador mssql-python.

Diferencias entre dialectos de SQL

Cuando migras de SQLite a Microsoft SQL, necesitas abordar dos cosas: reescribir tus sentencias SQL para Transact-SQL (T-SQL) y migrar tus datos.

La siguiente tabla mapea patrones comunes de SQLite a sus equivalentes en Microsoft SQL:

SQLite SQL Server (T-SQL) Notas
INTEGER PRIMARY KEY AUTOINCREMENT int IDENTITY(1,1) PRIMARY KEY Microsoft SQL usa IDENTITY para autoincremento.
TEXT nvarchar(255) o nvarchar(max) Indica siempre una longitud. Usa nvarchar para Unicode.
REAL float o decimal(18,2) Úsalo decimal para valores exactos como el dinero.
BLOB varbinary(max) Mismo comportamiento, nombre diferente.
BOOLEAN (almacenado como ENTERO) bit Ninguna de las dos bases de datos tiene un booleano nativo. Ambos almacenan 0/1.
DATETIME('now') GETDATE() o SYSDATETIME() SYSDATETIME() Proporciona mayor precisión.
LIMIT 10 OFFSET 20 OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY Requiere una ORDER BY cláusula.
\|\| (concat de cadena) + o CONCAT() CONCAT() gestiona NULL valores.
IFNULL(a, b) ISNULL(a, b) o COALESCE(a, b) COALESCE es estándar ANSI.
GROUP_CONCAT(col) STRING_AGG(col, ',') Disponible en SQL Server 2017+.
INSERT OR REPLACE INTO Instrucción MERGE SQLite elimina y vuelve a insertar; MERGE actualiza en el mismo lugar. Véase el ejemplo que sigue.
last_insert_rowid() OUTPUT INSERTED.id Usa OUTPUT en la sentencia INSERT. SCOPE_IDENTITY() también funciona, pero requiere un archivo separado SELECT.
typeof(x) SQL_VARIANT_PROPERTY(x, 'BaseType') Rara vez se necesita con el tipado estricto.

CREATE TABLE ejemplo

-- SQLite
CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    price REAL DEFAULT 0.0,
    created_at TEXT DEFAULT (datetime('now')),
    is_active BOOLEAN DEFAULT 1
);

-- SQL Server
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'products')
CREATE TABLE products (
    id int IDENTITY(1,1) PRIMARY KEY,
    name nvarchar(100) NOT NULL,
    price decimal(10,2) DEFAULT 0.0,
    created_at datetime2 DEFAULT SYSDATETIME(),
    is_active bit DEFAULT 1
);

Ejemplos de consultas

Paginación:

SQLite:

cursor.execute("SELECT * FROM products ORDER BY name LIMIT ? OFFSET ?", (10, 20))

MSSQL-Python:

cursor.execute(
    "SELECT * FROM Production.Product ORDER BY Name OFFSET ? ROWS FETCH NEXT ? ROWS ONLY",
    (20, 10)
)

El orden de los parámetros se invierte. Transact-SQL pone OFFSET antes FETCH NEXTde .

Upsert (insertar o actualizar):

SQLite:

cursor.execute("""
    INSERT OR REPLACE INTO settings (key, value)
    VALUES (?, ?)
""", (key, value))

MSSQL-Python:

cursor.execute("""
    MERGE #Settings AS target
    USING (SELECT ? AS [key], ? AS value) AS source
    ON target.[key] = source.[key]
    WHEN MATCHED THEN UPDATE SET value = source.value
    WHEN NOT MATCHED THEN INSERT ([key], value) VALUES (source.[key], source.value);
""", (key, value))

La USING cláusula define source.[key] y source.value como alias de columna. Las WHEN cláusulas hacen referencia a esos alias. Solo se necesitan dos ? marcadores.

Último ID insertado:

SQLite:

cursor.execute("INSERT INTO products (name) VALUES (?)", ("Widget",))
product_id = cursor.lastrowid

MSSQL-Python:

cursor.execute(
    "INSERT INTO #Products (Name) OUTPUT INSERTED.ProductID VALUES (?)",
    ("Widget",)
)
product_id = cursor.fetchval()

Actualizar código de conexión

Reemplace sqlite3.connect() con mssql_python.connect().

SQLite:

import sqlite3

def get_connection():
    conn = sqlite3.connect("myapp.db")
    conn.row_factory = sqlite3.Row
    return conn

MSSQL-Python:

import mssql_python

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

El acceso a filas funciona de forma similar. La factoría Row de SQLite devuelve filas similares a diccionarios con la sintaxis row["column"]. El controlador mssql-python devuelve objetos Row que admiten el mismo acceso mediante claves de cadena, además de acceso por atributo y por índice:

SQLite (con row_factory):

row["name"]

MSSQL-Python:

row["name"]   # String-key access, like SQLite
row.name      # Attribute access
row[0]        # Index access

Estilo del parámetro de actualización

Tanto SQLite como mssql-python usan ? como marcador de parámetro, por lo que la mayoría de las consultas funcionan sin cambios.

# Works with both sqlite3 and mssql-python
cursor.execute("SELECT * FROM Production.Product WHERE Name = ?", ("Adjustable Race",))

La única diferencia: SQLite permite parámetros con nombre con :name sintaxis. El controlador mssql-python utiliza %(name)s en su lugar.

SQLite:

cursor.execute("SELECT * FROM products WHERE id = :id", {"id": 42})

MSSQL-Python:

cursor.execute("SELECT * FROM Production.Product WHERE ProductID = %(id)s", {"id": 42})

Migrar datos existentes

Consideraciones importantes al migrar datos de SQLite a Microsoft SQL:

  • Microsoft SQL almacena los valores nvarchar(max) y varbinary(max) como objetos grandes (LOBs), que son más lentos de leer y escribir que los datos en línea. Mantén las columnas de cadena en nvarchar(4000) o menos siempre que tus datos lo permitan. SQL Server almacena estos valores directamente en la fila de datos, evitando la sobrecarga de LOB.
  • El mapeo de tipos sigue las reglas de afinidad de tipos de SQLite. Revisa las tablas generadas tras la migración para ajustar el tamaño de las columnas (por ejemplo, nvarchar(100) en lugar de nvarchar(4000)) o añadir restricciones que SQLite no aplicaba.
  • Para las tablas SQLite que usan TEXT para almacenar fechas, puede que necesites analizar los valores en objetos Python datetime antes de insertarlos. Microsoft SQL espera valores de fecha y hora adecuados, no cadenas de texto.

Utiliza este script para leer el esquema y los datos de SQLite y crear tablas coincidentes en Microsoft SQL:

import sqlite3
import mssql_python

# SQLite type affinity -> Microsoft SQL type
# Keywords follow SQLite's type affinity rules
TYPE_MAP = {
    "INT": "bigint",
    "CHAR": "nvarchar(4000)",
    "CLOB": "nvarchar(max)",
    "TEXT": "nvarchar(4000)",
    "BLOB": "varbinary(max)",
    "REAL": "float",
    "FLOA": "float",
    "DOUB": "float",
}


def map_type(sqlite_type: str) -> str:
    """Map a SQLite column type to a Microsoft SQL type."""
    upper = (sqlite_type or "TEXT").upper()
    for prefix, sql_type in TYPE_MAP.items():
        if prefix in upper:
            return sql_type
    return "decimal(18,6)"  # NUMERIC affinity (default)


# Connect to both databases
sqlite_conn = sqlite3.connect("myapp.db")
sql_conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes;"
)

# Get list of tables from SQLite
sqlite_cur = sqlite_conn.cursor()
sqlite_cur.execute(
    "SELECT name FROM sqlite_master "
    "WHERE type='table' AND name NOT LIKE 'sqlite_%'"
)
tables = [row[0] for row in sqlite_cur.fetchall()]

sql_cursor = sql_conn.cursor()

for table in tables:
    # Read column info from SQLite
    sqlite_cur.execute(f"PRAGMA table_info([{table}])")
    columns = sqlite_cur.fetchall()
    # columns: (cid, name, type, notnull, default_value, pk)

    # Build CREATE TABLE statement
    col_defs = []
    for col in columns:
        name, col_type, notnull, pk = col[1], col[2], col[3], col[5]
        sql_type = map_type(col_type)
        parts = [f"[{name}] {sql_type}"]
        if notnull:
            parts.append("NOT NULL")
        if pk:
            parts.append("PRIMARY KEY")
        col_defs.append(" ".join(parts))

    create_sql = f"CREATE TABLE [{table}] ({', '.join(col_defs)})"
    sql_cursor.execute(
        f"IF OBJECT_ID('{table}','U') IS NULL " + create_sql
    )
    sql_conn.commit()

    # Read all rows from SQLite
    sqlite_cur.execute(f"SELECT * FROM [{table}]")
    rows = sqlite_cur.fetchall()

    if not rows:
        print(f"  {table}: created (empty)")
        continue

    # Use bulkcopy for fast insert
    result = sql_cursor.bulkcopy(table, rows)
    print(f"  {table}: {result['rows_copied']} rows copied")

sql_conn.commit()
sqlite_conn.close()
sql_conn.close()

Diferencias de características

Tras la migración, tu aplicación obtiene acceso a funciones de Microsoft SQL que SQLite no soporta:

Feature SQLite SQL Server
Escrituras simultáneas Escritor solitario a la vez Concurrencia total con bloqueo a nivel de fila
Autenticación Solo permisos de archivo autenticación SQL, autenticación de Windows, Microsoft Entra ID
Procedimientos almacenados No soportado Programación completa en T-SQL
Encryption No integrado TLS en tránsito, TDE en reposo
Transactions Puntos de salvaguarda, niveles básicos de aislamiento Niveles de aislamiento completo, transacciones distribuidas
Compatibilidad con JSON json_extract() OPENJSON(), JSON_VALUE(), FOR JSON
Búsqueda de texto completo Extensión FTS5 Indexación de texto completo incorporada
Tamaño máximo de base de datos ~281 TB (límite práctico inferior) 524 PB
Agrupación de conexiones N/A (en proceso) Integrado con mssql-python