Aangepaste type-converters met mssql-python

Standaard zet de mssql-python-driver Microsoft SQL-datatypes om naar geschikte Python-types (zie Data type mappings). Het output converter-systeem laat je dit gedrag voor specifieke SQL-typen overschrijven, waardoor het volgende wordt ingeschakeld:

  • Aangepaste typetransformaties (bijvoorbeeld geld naar een float in plaats van Decimal)
  • Integratie met bibliotheken van derden (bijvoorbeeld ruimtelijke data naar Shapely-objecten)
  • Bedrijfsspecifieke opmaak (bijvoorbeeld data naar aangepaste stringformaten)

Uitgangsomzetters voor registers

Gebruik add_output_converter() om een converterfunctie te registreren. De sleutel is ofwel een Python-type dat overeenkomt met de type_code in cursor.description, of een geheel getal als ODBC SQL-typecode, zoals 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

Kies tussen een Python-type key en een SQL-type code-key

De twee belangrijkste vormen verschillen in hoe ze precies kolommen selecteren:

  • Een Python-type-sleutel is van toepassing op elk SQL-type dat aan dat Python-type wordt gekoppeld. Het registreren van een converter voor Decimal transformeert decimal-, numeric-, money- en smallmoney-kolommen.

  • Een sleutel van het SQL-integertype komt overeen met de ODBC-typecode die door de kolom wordt gerapporteerd, die specifieker is maar niet altijd overeenkomt met één enkel SQL-type. SQL_NUMERIC transformeert alleen numerieke kolommen. SQL_DECIMAL Transformeert de kolommen decimaal, geld en kleingeld , omdat alle drie dezelfde code rapporteren.

Gebruik een integer-key wanneer een Python-type sleutel breder is dan je wilt, of wanneer je code porteert van pyodbc, dat integer-keys gebruikt. Gebruik een Python-type key wanneer je één converter wilt die een hele familie van SQL-typen omvat.

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')

Resolutievolgorde van converters

Voor elke kolom selecteert de driver hooguit één converter, in deze volgorde:

  1. De converter registreerde zich voor de integer ODBC SQL-typecode van de kolom.

  2. De converter registreerde voor het Python-type in cursor.description.

  3. De converter is geregistreerd voor SQL_WVARCHAR, maar alleen wanneer het Python-type van de kolom str of bytes is.

Signatuur van de omzetterfunctie

Wanneer je een converter registreert met een sleutel van het type Python, moet het type dat je doorgeeft aan add_output_converter() overeenkomen met een van de volgende Python-typen. De kolom Ontvangst van de Converter toont wat de driver aan jouw functie doorgeeft:

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

Opmerking

String-type converters (strsleutel) ontvangen raw bytes in UTF-16LE-codering, niet in gedecodeerde Python-strings. Alle andere types ontvangen het reeds geconverteerde Python-object.

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)

Converters beheren

Gebruik deze methoden om converters die al op een verbinding geregistreerd zijn te inspecteren of te verwijderen.

Haal een bestaande converter

Haal de converterfunctie op die momenteel is geregistreerd voor een type:

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

Verwijder een converter

Verwijder een converter zodat de driver terugkeert naar standaardconversie voor dat type:

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

Maak alle converters schoon

Zet alle geregistreerde converters terug om de standaardtypetoewijzingen van Microsoft SQL te gebruiken:

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

Algemene converterpatronen

Deze voorbeelden tonen de meest benodigde converterfuncties.

Zet VARCHAR om naar hoofdletters

String-type converters ontvangen ruwe bytes in UTF-16LE-codering:

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

Zet een geldbedrag om naar float

Standaard geeft de driver de typen DECIMAL/NUMERIC terug als decimal.Decimal. Converteer naar float:

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'>

Parse JSON-gegevens

Microsoft SQL Server kan JSON als tekst opslaan. Automatisch parsen naar Python-objecten:

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

Waarschuwing

Dit voorbeeld converteert ALLE stringwaarden (nvarchar, varchar, xml). JSON-parsing wordt geprobeerd op elke stringkolom. In de praktijk kun je JSON-parsing selectief toepassen in plaats van als blanket-converter.

Aangepaste datum-tijdopmaak

Converteer de datumtijd naar het ISO-stringformaat:

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

Beveiligingsoverwegingen

Waarschuwing

Wanneer je een outputconverter registreert, draait de functie die je levert op elke overeenkomende databasewaarde.

  • Registreer alleen converters van vertrouwde code.
  • Accepteer nooit converterfuncties van gebruikersinvoer.
  • Kwaadaardige converters kunnen willekeurige code uitvoeren of data lekken.
# 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)

Voorbeeld: Integratie van ruimtelijke data

Integreer ruimtelijke data met de Shapely-bibliotheek:

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})")

Opmerking

Het registreren van een bytes converter beïnvloedt ALLE binaire kolommen (variabel, geografie, geometrie). Gebruik clear_output_converters() na ruimtelijke queries als je ook niet-ruimtelijke binaire data opvraagt.

Voorbeeld: Aangepaste rijopmaak

Maak een dataclass-converter:

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}")

Omzetterscope en levensduur

  • Connection-scoped: De driver registreert converters per verbinding, niet globaal.
  • Persistent: Converters blijven actief totdat je ze verwijdert of de verbinding sluit.
  • Niet geërfd: Nieuwe verbindingen erven geen converters van andere verbindingen.
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

Prestatie-overwegingen

  • De driver roept converters aan voor elke waarde van het geregistreerde type.
  • Houd de omzetterfuncties efficiënt.
  • Voor zoekopdrachten met een groot volume moet je overwegen of conversie noodzakelijk is.
  • Analyseer de prestaties als converters de doorvoer beïnvloeden.
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