Benutzerdefinierte Typkonverter mit mssql-python

Standardmäßig wandelt der mssql-python-Treiber Microsoft SQL-Datentypen in geeignete Python-Typen um (siehe Datentypabbildungen). Das Output-Konvertersystem erlaubt es, dieses Verhalten für bestimmte SQL-Typen zu überschreiben und so Folgendes zu aktivieren:

  • Benutzerdefinierte Typtransformationen (zum Beispiel von money zu float statt zu Decimal)
  • Integration mit Drittanbieter-Bibliotheken (zum Beispiel räumliche Daten zu Shapely-Objekten)
  • Unternehmensspezifische Formatierung (zum Beispiel Datumsangaben in benutzerdefinierte Zeichenfolgenformate)

Ausgangswandler registrieren

Verwenden Sie add_output_converter(), um eine Konvertierungsfunktion zu registrieren. Der Schlüssel ist entweder ein Python-Typ, der mit dem type_code in cursor.descriptionübereinstimmt, oder ein ganzzahliger ODBC-SQL-Code wie mssql_python.SQL_DECIMAL:

import mssql_python
from datetime import datetime

def my_datetime_converter(value):
    """Convert datetime to a custom display string."""
    if value is None:
        return None
    return value.strftime("%B %d, %Y at %I:%M %p")

conn = mssql_python.connect(connection_string)
conn.add_output_converter(datetime, my_datetime_converter)

cursor = conn.cursor()
cursor.execute("SELECT CAST('2026-07-16T10:30:00' AS DATETIME2)")
print(cursor.fetchval())  # July 16, 2026 at 10:30 AM

Wählen Sie zwischen einem Python-Typschlüssel und einem SQL-Codeschlüssel

Die beiden Schlüsselformen unterscheiden sich darin, wie genau sie Spalten auswählen:

  • Ein Schlüssel eines Python-Typs gilt für jeden SQL-Typ, der diesem Python-Typ zugeordnet ist. Das Registrieren eines Konverters für Decimal konvertiert decimal-, numeric-, money- und smallmoney-Spalten.

  • Ein Schlüssel vom SQL-Typcode Integer entspricht dem ODBC-Typcode, den die Spalte meldet, der spezifischer ist, jedoch nicht immer einem einzelnen SQL-Typ entspricht. SQL_NUMERIC transformiert nur numerische Spalten. SQL_DECIMAL Es transformiert Dezimal-, Geld- und Kleingeld-Spalten , weil alle drei denselben Code angeben.

Verwende einen Integer-Schlüssel, wenn ein Python-Schlüssel breiter ist als du möchtest, oder wenn du Code von Pyodbc portierst, das Integer-Schlüssel verwendet. Verwende einen Python-Typschlüssel, wenn du einen Konverter für eine ganze Familie von SQL-Typen verwenden möchtest.

import mssql_python

def tag_decimal(value):
    return f"D:{value}"

conn.add_output_converter(mssql_python.SQL_DECIMAL, tag_decimal)

cursor.execute("""
    SELECT CAST(12.34 AS decimal(10,2)),
           CAST(56.78 AS numeric(10,2)),
           CAST(90.12 AS money)
""")
print(cursor.fetchone())  # ('D:12.34', Decimal('56.78'), 'D:90.1200')

Auflösungsreihenfolge des Konverters

Für jede Spalte wählt der Treiber höchstens einen Konverter aus, in folgender Reihenfolge:

  1. Der Konverter registrierte sich für den ganzzahligen ODBC-SQL-Typcode der Spalte.

  2. Der für den Python-Typ in cursor.description registrierte Konverter.

  3. Der Konverter wurde für SQL_WVARCHAR registriert, aber nur, wenn der Python-Typ der Spalte bytes oder str ist.

Funktionssignatur des Konverters

Wenn Sie einen Konverter mit einem Python-Typschlüssel registrieren, muss der Typ, den Sie an add_output_converter() übergeben, einem der folgenden Python-Typen entsprechen. Die Spalte Konverterempfänger zeigt an, was der Treiber an Ihre Funktion übergibt:

cursor.description type_code Konverter empfängt Microsoft SQL-Typen
int int tinyint, smallint, int, bigint
float float real, float
decimal.Decimal Decimal decimal, numeric, money, smallmoney
str bytes (UTF-16LE kodiert) char, varchar, nchar, nvarchar, , xml
datetime.datetime datetime datetime, datetime2, smalldatetime
datetime.date date date
bytes bytes varbinary, binary, geography, geometry
bool bool bit

Note

Konverter vom Typ String (str key) erhalten rohe bytes-Werte in UTF-16LE-Kodierung, nicht dekodierte Python-Strings. Alle anderen Typen erhalten das bereits konvertierte Python-Objekt.

from decimal import Decimal

def decimal_converter(value: Decimal | None) -> float | None:
    if value is None:
        return None
    # value is a Decimal object for numeric/money types
    return float(value)

Konverter verwalten

Verwenden Sie diese Methoden, um bereits registrierte Konverter in einer Verbindung zu inspizieren oder zu entfernen.

Holen Sie sich einen bestehenden Konverter

Holen Sie die derzeit für einen Typ registrierte Konverterfunktion ab:

converter = conn.get_output_converter(str)
if converter:
    print(f"Converter registered: {converter}")
else:
    print("Using default conversion")

Entfernen Sie einen Konverter

Einen Konverter deregistrieren, damit der Treiber für diesen Typ zur Standardumwandlung zurückkehrt:

conn.remove_output_converter(str)
# str columns now use default conversion

Alle Konverter löschen

Setze alle registrierten Konverter zurück, um die Standardtypzuordnungen von Microsoft SQL zu verwenden:

conn.clear_output_converters()
# All types now use default conversion

Gängige Konvertierungsmuster

Diese Beispiele zeigen die am häufigsten benötigten Wandlerfunktionen.

VARCHAR in Großbuchstaben umwandeln

Konverter vom Typ String erhalten Rohbytes in UTF-16LE-Kodierung:

def uppercase_converter(value):
    if value is None:
        return None
    return value.decode('utf-16-le').upper()

conn.add_output_converter(str, uppercase_converter)

cursor = conn.cursor()
cursor.execute("SELECT 'hello world'")
print(cursor.fetchval())  # HELLO WORLD

money zu float konvertieren

Standardmäßig gibt der Treiber die DECIMAL-/NUMERIC-Typen als zurück decimal.Decimal. In Float konvertieren:

from decimal import Decimal

def money_to_float(value):
    if value is None:
        return None
    return float(value)

conn.add_output_converter(Decimal, money_to_float)

cursor = conn.cursor()
cursor.execute("SELECT CAST(19.99 AS MONEY)")
result = cursor.fetchval()
print(type(result))  # <class 'float'>

JSON-Daten analysieren

Microsoft SQL Server kann JSON als Text speichern. Automatisch zu Python-Objekten parsen:

import json

def json_converter(value):
    if value is None:
        return None
    text = value.decode('utf-16-le')
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return text  # Return as string if not valid JSON

conn.add_output_converter(str, json_converter)

cursor = conn.cursor()
cursor.execute("SELECT '{\"name\": \"Widget\", \"price\": 19.99}'")
data = cursor.fetchval()
print(data['name'])  # Widget

Caution

Dieses Beispiel konvertiert ALLE String-Werte (nvarchar, varchar, xml). JSON-Parsing wird bei jeder Zeichenkettenspalte versucht. In der Praxis wenden Sie JSON-Parsing selektiv an, anstatt als Blanket-Konverter.

Benutzerdefinierte Datetime-Formatierung

Date-Time in das ISO-String-Format umwandeln:

from datetime import datetime

def datetime_to_iso(value):
    if value is None:
        return None
    return value.isoformat()

conn.add_output_converter(datetime, datetime_to_iso)

cursor = conn.cursor()
cursor.execute("SELECT TOP 1 ModifiedDate FROM Production.Product")
print(cursor.fetchval())  # 2014-02-08T10:01:36.827000

Sicherheitsüberlegungen

Caution

Wenn du einen Ausgabekonverter registrierst, läuft die von dir bereitgestellte Funktion auf jedem passenden Datenbankwert.

  • Registrieren Sie nur Konverter aus vertrauenswürdigem Code.
  • Akzeptieren Sie niemals Konverterfunktionen aus Benutzereingaben.
  • Bösartige Konverter könnten beliebigen Code ausführen oder Daten durchsickern.
# DANGEROUS - never do this
def unsafe_example(user_converter_code):
    converter_func = eval(user_converter_code)  # Security risk!
    conn.add_output_converter(str, converter_func)

Beispiel: Integration räumlicher Daten

Integrieren Sie räumliche Daten in die Shapely-Bibliothek:

from shapely import wkb

def geometry_converter(value):
    """Convert WKB binary to Shapely geometry object."""
    if value is None:
        return None
    return wkb.loads(value)

conn.add_output_converter(bytes, geometry_converter)

# Query must use STAsBinary() to get standard WKB format
cursor = conn.cursor()
cursor.execute(
    "SELECT SpatialLocation.STAsBinary() FROM Person.Address "
    "WHERE SpatialLocation IS NOT NULL AND City = 'Seattle'"
)
for row in cursor:
    shape = row[0]
    print(f"Point: ({shape.x:.4f}, {shape.y:.4f})")

Note

Die Registrierung eines Konverters bytes beeinflusst ALLE Binärspalten (varibinär, geografisch, geometrisch). Verwenden Sie clear_output_converters() nach räumlichen Abfragen, wenn Sie auch nicht räumliche Binärdaten abfragen.

Beispiel: Benutzerdefinierte Zeilenformatierung

Erstellen Sie einen Datenklassenkonverter:

from dataclasses import dataclass

@dataclass
class Product:
    id: int
    name: str
    price: float

def fetch_products_as_dataclass(cursor):
    """Convert rows to Product dataclass instances."""
    cursor.execute(
        "SELECT ProductID, Name, ListPrice FROM Production.Product "
        "WHERE ListPrice > 0"
    )
    results = []
    for row in cursor:
        results.append(Product(
            id=row.ProductID,
            name=row.Name,
            price=float(row.ListPrice)
        ))
    return results

products = fetch_products_as_dataclass(cursor)
for p in products[:5]:
    print(f"{p.name}: ${p.price:.2f}")

Umfang und Lebensdauer des Konverters

  • Verbindungsbezogen: Der Treiber registriert Konverter pro Verbindung, nicht global.
  • Persistent: Konverter bleiben aktiv, bis du sie entfernst oder die Verbindung schließt.
  • Nicht geerbt: Neue Verbindungen übernehmen keine Konverter von anderen Verbindungen.
conn1 = mssql_python.connect(connection_string)
conn1.add_output_converter(str, uppercase_converter)

conn2 = mssql_python.connect(connection_string)
# conn2 does NOT have the converter registered

Leistungsüberlegungen

  • Der Treiber ruft für jeden Wert des registrierten Typs Konverter auf.
  • Halte die Wandlerfunktionen effizient.
  • Für Anfragen mit hohem Volumen überlegen Sie, ob eine Konvertierung notwendig ist.
  • Profilleistung, wenn Konverter den Durchsatz beeinflussen.
import time

def slow_converter(value):
    time.sleep(1)  # Avoid time consuming routines like this - adds 1 second for each value processed!
    return str(value) if value else None