Trabajo con datos binarios

El controlador mssql-python soporta binary, varbinary, y image tipos de datos para Microsoft SQL.

Microsoft SQL almacena datos binarios en estos tipos de columnas:

Tipo Descripción Tamaño máximo
binary(n) Datos binarios de longitud fija. 8.000 bytes
varbinary(n) Datos binarios de longitud variable. 8.000 bytes
varbinary(max) Datos binarios grandes. 2 GB
image Datos binarios grandes heredados (obsoletos). 2 GB

El controlador mssql-python devuelve datos binarios como objetos Pythonbytes.

Cuándo almacenar el binario en la base de datos frente al sistema de archivos:

  • Guarda en la base de datos cuando los archivos son pequeños (menos de 1 MB), la consistencia transaccional con otros datos es importante, o cuando necesitas hacer copias de seguridad de datos y archivos juntos.
  • Guarda en el sistema de archivos (o Azure Blob Storage) cuando los archivos son grandes (más de 1 MB), necesitas entrega de CDN o necesitas servir archivos directamente a clientes sin viajes de ida y vuelta a bases de datos.
  • Como punto intermedio, la función FILESTREAM de Microsoft SQL almacena datos en el sistema de archivos con consistencia transaccional.

Insertar datos binarios

Pasa objetos Python bytes como parámetros; el controlador los asigna a columnas varbinary.

Insertar bytes directamente

Crea un objeto Python bytes y pásalo como parámetro de consulta:

import mssql_python

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

# Create a table to hold binary documents
cursor.execute("""
    CREATE TABLE #Documents (
        ID INT IDENTITY PRIMARY KEY,
        Name NVARCHAR(255),
        Content VARBINARY(MAX),
        Size INT NULL,
        ContentHash VARBINARY(32) NULL
    )
""")

# Binary data as bytes
binary_data = b'\x00\x01\x02\x03\x04\x05'

cursor.execute(
    "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
    {"name": "sample.bin", "content": binary_data}
)
conn.commit()

Insertar desde el archivo

Lee un archivo en modo binario e inserta su contenido:

def insert_file(cursor, file_path: str, name: str):
    """Insert a file as binary data."""
    with open(file_path, "rb") as f:
        content = f.read()
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content, Size) VALUES (%(name)s, %(content)s, %(size)s)",
        {"name": name, "content": content, "size": len(content)}
    )

insert_file(cursor, "image.png", "profile_picture.png")
conn.commit()

Insertar archivo de imagen

Almacenar archivos de imagen con metadatos detectando los tipos MIME a partir de extensiones de archivo:

# Create a table to hold images
cursor.execute("""
    CREATE TABLE #Images (
        ID INT IDENTITY PRIMARY KEY,
        FileName NVARCHAR(255),
        FileSize INT NULL,
        ContentType NVARCHAR(100) NULL,
        Width INT NULL,
        Height INT NULL,
        ImageData VARBINARY(MAX),
        Description NVARCHAR(MAX) NULL
    )
""")

def insert_image(cursor, image_path: str, description: str):
    """Insert an image into the database."""
    import os
    
    with open(image_path, "rb") as f:
        image_data = f.read()
    
    cursor.execute("""
        INSERT INTO #Images (FileName, FileSize, ContentType, ImageData, Description)
        VALUES (%(filename)s, %(filesize)s, %(content_type)s, %(data)s, %(desc)s)
    """, {
        "filename": os.path.basename(image_path),
        "filesize": len(image_data),
        "content_type": get_content_type(image_path),
        "data": image_data,
        "desc": description
    })

def get_content_type(path: str) -> str:
    """Determine MIME type from file extension."""
    ext = path.lower().split(".")[-1]
    types = {
        "png": "image/png",
        "jpg": "image/jpeg",
        "jpeg": "image/jpeg",
        "gif": "image/gif",
        "pdf": "application/pdf",
    }
    return types.get(ext, "application/octet-stream")

Recuperación de datos binarios

El controlador devuelve los valores de las columnas varbinary y binary como objetos Python bytes.

Obtener columna binaria

Consultar columnas binarias y examinar el objeto bytes devuelto:

cursor.execute(
    "SELECT LargePhotoFileName, LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoID = %(id)s",
    {"id": 70}
)
row = cursor.fetchone()

# LargePhoto is bytes
print(type(row.LargePhoto))  # <class 'bytes'>
print(len(row.LargePhoto))   # Number of bytes

Guardar en archivo

Recuperar datos binarios y escribirlos en el disco:

def save_photo(cursor, photo_id: int, output_path: str):
    """Retrieve binary data and save to file."""
    cursor.execute(
        "SELECT LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoID = %(id)s",
        {"id": photo_id}
    )
    row = cursor.fetchone()
    
    if row and row.LargePhoto:
        with open(output_path, "wb") as f:
            f.write(row.LargePhoto)
        print(f"Saved {len(row.LargePhoto)} bytes to {output_path}")
    else:
        print("Photo not found or empty")

save_photo(cursor, 70, "output.gif")

Exportar filas binarias a archivos

Exportar múltiples filas binarias a archivos locales:

import os

def export_photos(cursor, output_dir: str):
    """Export product photos to a directory."""
    os.makedirs(output_dir, exist_ok=True)
    
    cursor.execute("""
        SELECT ProductPhotoID, LargePhotoFileName, LargePhoto
        FROM Production.ProductPhoto
        WHERE ProductPhotoID > 1
    """)
    
    count = 0
    for row in cursor:
        output_path = os.path.join(output_dir, f"{row.ProductPhotoID}_{row.LargePhotoFileName}")
        with open(output_path, "wb") as f:
            f.write(row.LargePhoto)
        count += 1
    
    print(f"Exported {count} photos to {output_dir}")

export_photos(cursor, "./photos")

Insertar valores binarios NULL

Pase None para insertar un SQL NULL en una columna binaria. Como #Documents es una tabla temporal, declare los tipos de parámetro mediante setinputsizes() primero para que el controlador vincule Content como varbinary:

cursor.setinputsizes([(mssql_python.SQL_WVARCHAR, 255, 0), (mssql_python.SQL_VARBINARY, 0, 0)])
cursor.execute(
    "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
    {"name": "empty", "content": None}
)

Note

El controlador normalmente infiere los tipos de parámetros a través de SQLDescribeParam, pero no puede resolver metadatos de tipo para una tabla temporal o una variable de tabla. Insertar None en una columna binary o varbinary de un objeto temporal sin setinputsizes() genera ProgrammingError: Implicit conversion from data type varchar to varbinary(max) is not allowed. Pasa una entrada por parámetro, en orden, y usa una constante de tipo SQL, como mssql_python.SQL_VARBINARY para la columna binaria. Para una tabla normal (permanente), el controlador resuelve el tipo automáticamente y puedes pasar None directamente.

Datos binarios grandes

Para archivos de más de unos pocos megabytes, inserta el contenido completo en una sola escritura varbinary(max) en lugar de leer el archivo en pequeños fragmentos.

Transmitir archivos grandes

Para archivos grandes, procesa en bloques:

def insert_large_file(conn, table: str, file_path: str, chunk_size: int = 8192):
    """Insert large file in chunks using updatetext-style approach."""
    # Validate table name to prevent SQL injection
    import re
    if not re.match(r'^#{0,2}[A-Za-z_][A-Za-z0-9_.]*$', table):
        raise ValueError(f"Invalid table name: {table}")

    cursor = conn.cursor()
    
    # Get file size
    import os
    file_size = os.path.getsize(file_path)
    
    # Insert initial row with empty binary
    cursor.execute(f"""
        INSERT INTO {table} (Name, Content, Size)
        VALUES (%(name)s, 0x, %(size)s);
        SELECT SCOPE_IDENTITY();
    """, {"name": os.path.basename(file_path), "size": file_size})
    
    row_id = cursor.fetchval()
    
    # For modern Microsoft SQL, better to use varbinary(max) and single insert
    # This example shows streaming approach
    with open(file_path, "rb") as f:
        content = f.read()
    
    cursor.execute(f"""
        UPDATE {table} SET Content = %(content)s WHERE ID = %(id)s
    """, {"content": content, "id": row_id})
    
    conn.commit()
    return row_id

Usa copia masiva para datos binarios

Utiliza el bulkcopy() método para insertar eficientemente múltiples archivos binarios en una sola operación.

Importante

bulkcopy() carga los datos a través de una conexión separada al servidor, por lo que la tabla de destino ya debe existir y estar confirmada y visible para otras sesiones. Si creas la tabla en el mismo script con autocommit desactivado, ejecuta conn.commit() antes de bulkcopy(). Las tablas temporales locales (#name) no están soportadas porque son privadas de la sesión que las creó. Un destino no confirmado o inalcanzable hace que bulkcopy() falle por un tiempo de espera de Failed to retrieve destination metadata.

Por defecto, bulkcopy() asigna cada valor de una fila a una columna de tabla por posición ordinal, por lo que cada columna debe tener un valor en orden. Cuando la tabla tiene una columna identidad o solo llenas algunas columnas, pasa column_mappings para nombrar explícitamente las columnas destino. De lo contrario, los valores se desplazan a columnas incorrectas y bulkcopy() falla.

def bulk_insert_files(conn, table: str, file_paths: list[str]):
    """Bulk insert multiple binary files."""
    import os

    data = []
    for path in file_paths:
        with open(path, "rb") as f:
            content = f.read()
        data.append((os.path.basename(path), content, len(content)))

    cursor = conn.cursor()
    result = cursor.bulkcopy(table, data, column_mappings=["Name", "Content", "Size"])
    conn.commit()
    return result["rows_copied"]

Operaciones de datos binarios

Utiliza la función de HASHBYTES Microsoft SQL para comparar contenido binario sin obtener valores completos.

Comparar datos binarios

Encuentra filas binarias que compartan contenido idéntico calculando y comparando hashes de contenido en Microsoft SQL Server:

# Find rows with identical binary content by hash
cursor.execute("""
    SELECT LargePhotoFileName, HASHBYTES('SHA2_256', LargePhoto) AS PhotoHash
    FROM Production.ProductPhoto
    WHERE ProductPhotoID > 1
""")

hashes = {}
for row in cursor:
    hash_value = row.PhotoHash  # bytes
    if hash_value in hashes:
        print(f"Duplicate: {row.LargePhotoFileName} matches {hashes[hash_value]}")
    else:
        hashes[hash_value] = row.LargePhotoFileName

Calcular hash en Python

Calcula los hashes SHA-256 en Python y alárdalos junto con datos binarios:

import hashlib

def insert_with_hash(cursor, name: str, content: bytes):
    """Insert binary data with computed hash."""
    content_hash = hashlib.sha256(content).digest()
    
    cursor.execute("""
        INSERT INTO #Documents (Name, Content, ContentHash)
        VALUES (%(name)s, %(content)s, %(hash)s)
    """, {"name": name, "content": content, "hash": content_hash})

Codificar y decodificar datos binarios

Convierte datos binarios a y desde cadenas codificadas en base64:

import base64

# Store base64-encoded string
def insert_base64(cursor, name: str, base64_data: str):
    """Insert base64-encoded data as binary."""
    binary_data = base64.b64decode(base64_data)
    cursor.execute(
        "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
        {"name": name, "content": binary_data}
    )

# Retrieve as base64
def get_as_base64(cursor, doc_id: int) -> str:
    """Retrieve binary data as base64 string."""
    cursor.execute("SELECT Content FROM #Documents WHERE ID = %(id)s", {"id": doc_id})
    row = cursor.fetchone()
    return base64.b64encode(row.Content).decode("utf-8")

Trabajo con formatos binarios específicos

Estos ejemplos muestran cómo validar e insertar formatos de archivo comunes antes de almacenarlos.

Documentos PDF

Verifica las firmas de archivos PDF antes de insertarlas:

def insert_pdf(cursor, pdf_path: str, title: str):
    """Insert a PDF document."""
    with open(pdf_path, "rb") as f:
        pdf_data = f.read()
    
    # Verify it's a PDF (magic bytes)
    if not pdf_data.startswith(b'%PDF'):
        raise ValueError("Not a valid PDF file")
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content) VALUES (%(title)s, %(data)s)",
        {"title": title, "data": pdf_data}
    )

Imágenes con PIL/Pillow

Redimensiona las imágenes usando Pillow (PIL) antes de almacenarlas para ahorrar espacio:

from PIL import Image
import io
import os

def insert_resized_image(cursor, image_path: str, max_size: tuple = (800, 600)):
    """Insert a resized image."""
    # Open and resize
    img = Image.open(image_path)
    img.thumbnail(max_size, Image.LANCZOS)
    
    # Convert to bytes
    buffer = io.BytesIO()
    img.save(buffer, format=img.format or "PNG")
    image_bytes = buffer.getvalue()
    
    cursor.execute("""
        INSERT INTO #Images (FileName, Width, Height, ImageData)
        VALUES (%(name)s, %(width)s, %(height)s, %(data)s)
    """, {
        "name": os.path.basename(image_path),
        "width": img.width,
        "height": img.height,
        "data": image_bytes
    })

def get_image_as_pil(cursor, image_id: int) -> Image.Image:
    """Retrieve image as PIL Image object."""
    cursor.execute("SELECT ImageData FROM #Images WHERE ID = %(id)s", {"id": image_id})
    row = cursor.fetchone()
    return Image.open(io.BytesIO(row.ImageData))

Datos comprimidos

Reduce el almacenamiento comprimiendo datos binarios con gzip antes de insertarlos:

import gzip

def insert_compressed(cursor, name: str, data: bytes):
    """Insert data with gzip compression."""
    compressed = gzip.compress(data)
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content, Size) VALUES (%(name)s, %(content)s, %(size)s)",
        {"name": name, "content": compressed, "size": len(data)}
    )

def get_decompressed(cursor, data_id: int) -> bytes:
    """Retrieve and decompress data."""
    cursor.execute(
        "SELECT Content FROM #Documents WHERE ID = %(id)s",
        {"id": data_id}
    )
    row = cursor.fetchone()
    return gzip.decompress(row.Content)

procedimientos recomendados

Aplica estas pautas para elegir el tipo de columna y la estrategia de almacenamiento correctos para los datos binarios.

Utiliza tipos de columnas apropiados

Elige el tipo de columna según las características de tus datos:

Para diferentes casos de uso binarios, seleccione el tipo de dato Microsoft SQL apropiado:

-- For small fixed-size binary (for example, hashes, UUIDs)
binary(32)      -- SHA-256 hash

-- For variable-size binary up to 8KB (for example, thumbnails, small icons)
varbinary(8000)

-- For large binary data (for example, documents, images)
varbinary(max)  -- Up to 2GB

Consideremos FILESTREAM para archivos grandes

Para archivos grandes (más de 1 MB), consideremos la función FILESTREAM de SQL de Microsoft, que almacena datos en el sistema de archivos. FILESTREAM requiere configuración en el lado del servidor antes de su uso:

# FILESTREAM-enabled databases store large binaries more efficiently
# Access is still through normal queries but storage is file-based
large_binary_data = b"\x25\x50\x44\x46" + b"\x00" * 100  # sample data

cursor.execute("""
    CREATE TABLE ##FileStreamDemo (Name NVARCHAR(100), Document VARBINARY(MAX))
""")
cursor.execute("""
    INSERT INTO ##FileStreamDemo (Name, Document)
    VALUES (%(name)s, %(content)s)
""", {"name": "large_doc.pdf", "content": large_binary_data})

cursor.execute("SELECT Name, DATALENGTH(Document) AS DocSize FROM ##FileStreamDemo")
row = cursor.fetchone()
print(f"{row.Name}: {row.DocSize} bytes")

Validar datos binarios

Valida los tipos de archivo comprobando los bytes mágicos (firmas de archivo) antes de almacenar datos binarios:

def insert_safe_image(cursor, name: str, data: bytes):
    """Insert image with validation."""
    # Check file signatures (magic bytes)
    signatures = {
        b'\x89PNG': 'image/png',
        b'\xff\xd8\xff': 'image/jpeg',
        b'GIF87a': 'image/gif',
        b'GIF89a': 'image/gif',
    }
    
    content_type = None
    for sig, mime in signatures.items():
        if data.startswith(sig):
            content_type = mime
            break
    
    if content_type is None:
        raise ValueError("Unknown or unsupported image format")
    
    cursor.execute("""
        INSERT INTO #Images (FileName, ContentType, ImageData)
        VALUES (%(name)s, %(type)s, %(data)s)
    """, {"name": name, "type": content_type, "data": data})