Custom Fetch Mapper#

This example compares values returned by PyExasol’s default WebSocket fetch mapper with values returned by a custom mapper. The custom mapper converts VARCHAR values to uppercase and leaves other values unchanged. It creates and seeds the PYEXASOL_DBAPI_TYPE_CONVERSION table when it is not already populated, then prints both rows side by side.

Configure the connection in examples/config.py or with the environment variables documented by the Connection Configuration example.

Run it from the repository root with:

poetry run python -m examples.configuration.fetch_mapper
examples/configuration/fetch_mapper.py#
import pyexasol
from packaging.version import Version
from pyexasol.data_types.websocket_types import WebSocketDataType
from sqlalchemy import text

from examples.config import (
    DEFAULT_SCHEMA_NAME,
    ENGINE,
    SQL_ALCHEMY,
)

if Version(pyexasol.__version__) < Version("3.0.0"):
    raise RuntimeError("This demo won't work with PyExasol versions older than 3.0.0.")

TABLE_NAME = "PYEXASOL_DBAPI_TYPE_CONVERSION"
FULL_TABLE_NAME = f"{DEFAULT_SCHEMA_NAME}.{TABLE_NAME}"
BLUE = "\033[94m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET = "\033[0m"
DATA_TYPE_COLUMNS = (
    ("BOOLEAN_VALUE", "BOOLEAN"),
    ("CHAR_VALUE", "CHAR(5)"),
    ("DATE_VALUE", "DATE"),
    ("DECIMAL_FRACTION", "DECIMAL(18, 3)"),
    ("DECIMAL_INTEGER", "DECIMAL(18, 0)"),
    ("DOUBLE_VALUE", "DOUBLE"),
    ("GEOMETRY_VALUE", "GEOMETRY"),
    ("HASHTYPE_VALUE", "HASHTYPE(16 BYTE)"),
    ("INTERVAL_DAY_SECOND", "INTERVAL DAY(9) TO SECOND(6)"),
    ("INTERVAL_YEAR_MONTH", "INTERVAL YEAR(4) TO MONTH"),
    ("TIMESTAMP_LOCAL_VALUE", "TIMESTAMP WITH LOCAL TIME ZONE"),
    ("TIMESTAMP_VALUE", "TIMESTAMP(6)"),
    ("VARCHAR_VALUE", "VARCHAR(20)"),
)
EXASOL_TYPES = tuple(data_type for _, data_type in DATA_TYPE_COLUMNS)
CREATE_TABLE = f"""
CREATE TABLE IF NOT EXISTS {FULL_TABLE_NAME} (
    BOOLEAN_VALUE BOOLEAN,
    CHAR_VALUE CHAR(5),
    DATE_VALUE DATE,
    DECIMAL_FRACTION DECIMAL(18, 3),
    DECIMAL_INTEGER DECIMAL(18, 0),
    DOUBLE_VALUE DOUBLE,
    GEOMETRY_VALUE GEOMETRY,
    HASHTYPE_VALUE HASHTYPE(16 BYTE),
    INTERVAL_DAY_SECOND INTERVAL DAY(9) TO SECOND(6),
    INTERVAL_YEAR_MONTH INTERVAL YEAR(4) TO MONTH,
    TIMESTAMP_LOCAL_VALUE TIMESTAMP WITH LOCAL TIME ZONE,
    TIMESTAMP_VALUE TIMESTAMP(6),
    VARCHAR_VALUE VARCHAR(20)
)
"""
INSERT_ROW = f"""
INSERT INTO {FULL_TABLE_NAME} (
    BOOLEAN_VALUE,
    CHAR_VALUE,
    DATE_VALUE,
    DECIMAL_FRACTION,
    DECIMAL_INTEGER,
    DOUBLE_VALUE,
    GEOMETRY_VALUE,
    HASHTYPE_VALUE,
    INTERVAL_DAY_SECOND,
    INTERVAL_YEAR_MONTH,
    TIMESTAMP_LOCAL_VALUE,
    TIMESTAMP_VALUE,
    VARCHAR_VALUE
)
VALUES (
    TRUE,
    'xy',
    DATE '2026-09-11',
    42.125,
    42,
    42.5,
    'LINESTRING (10 20, 30 40)',
    '6ba7b8109dad11d180b400c04fd430c8',
    INTERVAL '3 04:05:06.654' DAY TO SECOND,
    INTERVAL '1-11' YEAR TO MONTH,
    TIMESTAMP '2026-09-11 12:34:56.123456',
    TIMESTAMP '2026-09-11 12:34:56.123456',
    'example'
)
"""


def custom_fetch_mapper(value, data_type):
    """Convert VARCHAR values to uppercase and leave other values unchanged."""
    if value is None:
        return None
    if data_type["type"] == WebSocketDataType.String.value:
        return value.upper()
    return value


def fetch_one_row(engine):
    """Fetch one row through SQLAlchemy."""
    with engine.connect() as connection:
        return connection.execute(text(f"SELECT * FROM {FULL_TABLE_NAME}")).fetchone()


def ensure_demo_table(engine):
    """Create and seed the demonstration table when it is not already populated."""
    SQL_ALCHEMY.create_schema(engine=engine, schema=DEFAULT_SCHEMA_NAME)
    with engine.begin() as connection:
        connection.execute(text(CREATE_TABLE))
        connection.execute(text(INSERT_ROW))


def print_comparison(default_row, custom_row):
    """Print default and custom DBAPI values side by side."""
    rows = [
        (data_type, repr(default_value), repr(custom_value))
        for data_type, default_value, custom_value in zip(
            EXASOL_TYPES, default_row, custom_row
        )
    ]
    type_width, default_width, custom_width = (
        max(map(len, column_values)) for column_values in zip(*rows)
    )
    rows_equal = default_row == custom_row
    comparison = "output is equal" if rows_equal else "output is not equal"

    print(
        f"{YELLOW}Rows equal: {rows_equal} "
        f"({comparison} between the default and custom mapper){RESET}"
    )
    print(
        f"\n{'Exasol type':<{type_width}} | "
        f"{BLUE}{'PyExasol default mapper':<{default_width}}{RESET} | "
        f"{GREEN}{'Custom mapper':<{custom_width}}{RESET}"
    )
    print(f"{'-' * type_width}-+-{'-' * default_width}-+-{'-' * custom_width}")
    for data_type, default_value, custom_value in rows:
        print(
            f"{data_type:<{type_width}} | "
            f"{BLUE}{default_value:<{default_width}}{RESET} | "
            f"{GREEN}{custom_value:<{custom_width}}{RESET}"
        )


default_engine = ENGINE
custom_engine = SQL_ALCHEMY.create_engine(
    connect_args={"fetch_mapper": custom_fetch_mapper},
)

ensure_demo_table(default_engine)
default_row = fetch_one_row(default_engine)
custom_row = fetch_one_row(custom_engine)
print_comparison(default_row, custom_row)