Microsoft SQL Server in Python

SQL Server queries with bound parameters, stored procedures and large result sets from Python services.

Microsoft SQL Server is widely used in enterprise environments, particularly in organizations with existing Microsoft infrastructure. Zato provides direct support for MS SQL through connection pools configured in the Dashboard.

MS SQL connections in Zato support both direct SQL execution and stored procedure calls, giving you flexibility in how you interact with your databases.

Creating a connection

In the Dashboard, navigate to Connections -> Outgoing -> SQL and create a new connection. Select MS SQL as the database type and provide your connection details.

After creating the connection, change the default password and test the connection before using it in your services.

Executing queries

Run SQL statements through conn.execute, passing parameters as a dictionary. Each :name marker in the statement is bound to the parameter of the same name by the server, so the values never become part of the SQL text. Results arrive as a list where each element is a dictionary representing one row.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetPassengers(Service):

    def handle(self):

        # Get the connection by name
        conn = self.out.sql['Airport MS SQL']

        # Query with a named parameter
        query = 'SELECT PassengerID, FullName, SeatNumber FROM Passengers WHERE FlightID = :flight_id'
        params = {'flight_id': 1042}

        # Execute returns a list of dicts
        result = conn.execute(query, params)

        self.response.payload = result

Response:

[
    {"PassengerID": 7781, "FullName": "John Smith", "SeatNumber": "12A"},
    {"PassengerID": 7782, "FullName": "Maria Johnson", "SeatNumber": "12B"}
]

A statement can also run a stored procedure with EXEC, in which case its first result set is what comes back:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetCompanyGuarantors(Service):
    """ Returns a list of company guarantors from the database.
    """
    def handle(self):

        # Connection to use
        conn = self.out.sql['Airport MS SQL']

        # A stored procedure invoked as a statement
        result = conn.execute('EXEC company.spGetCompanyGuarantors')

        # Build a list of guarantor objects from the result
        guarantors = []
        for row in result:
            guarantors.append({
                'company': row['CompanyName'],
                'company_id': row['CompanyIdentifier'],
                'guarantor': row['GuarantorName'],
                'email': row['GuarantorEmail'],
                'phone': row['GuarantorPhone']
            })

        # Return the guarantors to the caller
        self.response.payload = guarantors

Values such as datetime objects come back as Python objects and are formatted before they go into a JSON response:

# -*- coding: utf-8 -*-

# stdlib
import datetime

# Zato
from zato.server.service import Service

class GetWeatherForecast(Service):
    """ Returns weather forecast data from the data warehouse.
    """
    def handle(self):

        # Connection to use
        conn = self.out.sql['DataWarehouse']

        # Query to execute
        query = 'SELECT * FROM ssas.FlagWeatherForecast WHERE ForecastDate >= :since'
        params = {'since': '2026-01-01'}

        # Execute query
        data = conn.execute(query, params)

        # Format the data, converting datetime objects to strings
        result = []
        for item in data:
            formatted = {}
            for key, value in item.items():
                if isinstance(value, datetime.datetime):
                    formatted[key] = value.strftime('%Y-%m-%dT%H:%M:%S')
                else:
                    formatted[key] = value
            result.append(formatted)

        # Return the formatted data
        self.response.payload = result

Querying a single row

To fetch a single record, use conn.one - it returns that record as a dictionary and raises an exception if the query matches zero or more than one row:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetFlightById(Service):
    """ Returns a single flight by its ID.
    """
    input = 'flight_id'

    def handle(self):

        # Get the connection from the pool
        conn = self.out.sql['Airport MS SQL']

        # Build the query
        query = 'SELECT * FROM Flights WHERE FlightID = :flight_id'

        # Assign input data to query parameters
        params = {'flight_id': self.request.input.flight_id}

        # Run the query - returns a dict directly, raises if not found
        flight = conn.one(query, params)

        # Return the result to the caller
        self.response.payload = flight

When the record may or may not exist, conn.one_or_none is the right choice - it returns None for missing records rather than throwing an error:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class FindGateByCode(Service):
    """ Finds a gate by its code, returns None if not found.
    """
    input = 'code'

    def handle(self):

        # Get the connection from the pool
        conn = self.out.sql['Airport MS SQL']

        # Build the query
        query = 'SELECT * FROM Gates WHERE GateCode = :code'

        # Assign input data to query parameters
        params = {'code': self.request.input.code}

        # Run the query - returns dict or None if not found
        gate = conn.one_or_none(query, params)

        # Return the result or an error
        if gate:
            self.response.payload = gate
        else:
            self.response.payload = {'error': 'Gate not found'}
            self.response.status_code = 404

Insert, update, and delete

Writes work the same way - build the statement with :name markers and pass the values in a dictionary. A statement that produces no rows is committed as soon as it completes and returns an empty list.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class CreateBaggageClaim(Service):
    """ Creates a new baggage claim record for a flight.
    """
    input = 'flight_id', 'carousel_number'

    def handle(self):

        # Get the connection from the pool
        conn = self.out.sql['Airport MS SQL']

        # Build the INSERT statement
        query = """
            INSERT INTO BaggageClaims (FlightID, CarouselNumber, StartTime)
            VALUES (:flight_id, :carousel, GETDATE())
        """

        # Assign input data to query parameters
        params = {
            'flight_id': self.request.input.flight_id,
            'carousel': self.request.input.carousel_number
        }

        # Execute the insert - it is committed when it completes
        conn.execute(query, params)

        # Return success response
        self.response.payload = {'message': 'Baggage claim created'}
        self.response.status_code = 201
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class CloseGate(Service):
    """ Closes a gate by setting its status to closed.
    """
    input = 'gate_id'

    def handle(self):

        # Get the connection from the pool
        conn = self.out.sql['Airport MS SQL']

        # Build the UPDATE statement
        query = """
            UPDATE Gates
            SET Status = :status, ClosedAt = GETDATE()
            WHERE GateID = :gate_id
        """

        # Assign input data to query parameters
        params = {
            'gate_id': self.request.input.gate_id,
            'status': 'closed'
        }

        # Execute the update
        conn.execute(query, params)

        # Return success response
        self.response.payload = {'message': 'Gate closed'}

Multiple parameters

A query takes as many parameters as it has markers - every marker in the statement needs a value in the dictionary and every value needs a marker, otherwise the statement is refused before it reaches the database:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetDelayedFlights(Service):
    """ Returns flights of one airline delayed beyond a threshold.
    """
    input = 'airline_code', 'min_delay'

    def handle(self):

        # Get the connection from the pool
        conn = self.out.sql['Airport MS SQL']

        # Build the query with multiple markers
        query = """
            SELECT FlightID, AirlineCode, DelayMinutes
            FROM Flights
            WHERE AirlineCode = :airline_code AND DelayMinutes > :min_delay
            ORDER BY DelayMinutes DESC
        """

        # One value per marker
        params = {
            'airline_code': self.request.input.airline_code,
            'min_delay': self.request.input.min_delay
        }

        # Execute and return the rows
        self.response.payload = conn.execute(query, params)

Calling stored procedures

Stored procedures are called with conn.callproc(), which returns their result sets - a list with one element per result set, each element being a list of rows.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetFlightManifest(Service):
    """ Returns the passenger manifest for a flight.
    """
    input = 'flight_id'

    def handle(self):

        # Connection name from Dashboard
        conn_name = 'Airport MS SQL'

        # Get the connection
        conn = self.outgoing.sql.get(conn_name)

        # Stored procedure to call
        proc_name = 'spGetFlightManifest'

        # Build arguments list from input data
        args = [self.request.input.flight_id]

        # Call the procedure and get results
        data = conn.callproc(proc_name, args)

        # Return the data to the caller
        self.response.payload = data

Calling procedures with parameters

Pass data to stored procedures as a list of arguments:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class InsertRetailData(Service):
    """ Inserts retail submission data via a stored procedure.
    """
    input = 'data'

    def handle(self):

        # Connection to use
        conn_name = 'Retail Database'

        # Stored procedure name
        proc_name = 'company.spInsertRetailSubmission'

        # Get the connection
        conn = self.outgoing.sql.get(conn_name)

        try:
            # Call the procedure with input data as an argument
            response = conn.callproc(proc_name, [self.request.input.data])

            # Return success response
            self.response.payload = {'message': 'Data inserted successfully'}
            self.response.status_code = 201

        except Exception as e:
            # Log and return error
            self.logger.error(f'Error inserting data: {e}')
            self.response.payload = {'error': 'Unable to insert data'}
            self.response.status_code = 500

Processing large result sets

When a stored procedure returns many rows, use use_yield=True to process them one at a time without loading everything into memory:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ProcessLargeDataset(Service):
    """ Processes a large dataset row by row without loading all into memory.
    """
    def handle(self):

        # Connection to use
        conn_name = 'DataWarehouse'

        # Get the connection
        conn = self.outgoing.sql.get(conn_name)

        # Stored procedure and date range arguments
        proc_name = 'spGetAllTransactions'
        args = ['2026-01-01', '2026-12-31']

        # use_yield=True returns a generator instead of loading all rows
        data = conn.callproc(proc_name, args, use_yield=True)

        # Process each row individually
        processed_count = 0
        for row in data:
            self.invoke('transaction.process', data=row)
            processed_count += 1

        # Return the count of processed rows
        self.response.payload = {'processed': processed_count}

Audit log

The connection's Audit log dropdown sets what the audit log stores per statement - outcome and duration only, the SQL text, the text with parameters, or everything with rows. A procedure call is stored as EXEC name, its arguments as the parameters, its result sets as the rows. With use_yield=True the rows are not stored.

Error handling

Always handle potential database errors gracefully:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class SafeDatabaseCall(Service):
    """ Demonstrates proper error handling for database calls.
    """
    input = 'flight_id'

    def handle(self):

        # Connection to use
        conn = self.out.sql['Airport MS SQL']

        try:
            # Run the query
            query = 'SELECT * FROM FlightData WHERE FlightID = :flight_id'
            params = {'flight_id': self.request.input.flight_id}

            result = conn.execute(query, params)

            # Return the result
            self.response.payload = result

        except Exception as e:
            # Log error and return error response
            self.logger.error(f'Database error: {e}')
            self.response.payload = {'error': 'Database operation failed'}

How to avoid SQL injection attacks

Always use parameterized queries with named parameters (:param_name) rather than string formatting. This prevents SQL injection attacks and makes your queries more readable. With MS SQL, a :param_name marker with no matching parameter, or a parameter with no matching marker, is refused before the statement is sent to the database.

# Correct - parameterized query
query = 'SELECT * FROM users WHERE user_id = :user_id AND status = :status'
params = {'user_id': 123, 'status': 'active'}
result = conn.execute(query, params)

# Wrong - string formatting (vulnerable to SQL injection)
# query = f'SELECT * FROM users WHERE user_id = {user_id}'

More resources

Learn more