SQL Server DDL Execution – displaying errors below each statement

When working with large SQL Server databases, DBAs often need to execute DDL scripts containing thousands of lines of code. While SQLCMD provides a straightforward way to execute these scripts, its error reporting can be deficient when troubleshooting failures in large scripts. Recently, I encountered this issue while executing a large DDL file. The standard SQLCMD output only showed generic errors like “Function ‘fn_xyz’ already exists with the same name” without providing context about which specific statement caused the error. The line number displayed usually refers to the line number in the DDL statement and not the line number in the context of the larger DDL file.

For effective troubleshooting, I needed to see the SQL (DDL) statement that was executed followed by the error. Having these elements together would allow me to quickly correlate errors with their corresponding SQL statements, significantly reducing debugging time. To fulfill this need, I wrote a Python solution that provides granular execution control and comprehensive output logging. This approach offers several advantages:

Individual statement execution
Paired output of SQL and results
Clear separation between statements

The python code and sample successful and unsuccessful results are below.

Python code

import pyodbc
import os
from datetime import datetime

def execute_sql_file(input_file, output_file, server, user, password):
    # Get password from environment variable for security
    connection_string = f'DRIVER={{SQL Server}};SERVER={server};DATABASE=master;UID={user};PWD={password}'
    
    try:
        # Establish connection
        conn = pyodbc.connect(connection_string)
        cursor = conn.cursor()
        
        current_batch = []
        
        with open(input_file, 'r') as infile, \
             open(output_file, 'w') as outfile:
            
            # Write header with timestamp
            outfile.write(f"SQL Execution Results - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            outfile.write("-"*80 + "\n\n")
            
            for line in infile:
                # line = line.strip()
                line = line.rstrip('\n\r')  # removes both \n and \r
                
                # Skip empty lines
                # if not line:
                    # continue
                
                # If we hit a GO statement, execute the batch
                test_line = line
                if test_line.strip().lower() == 'go':
                    if current_batch:
                        sql_statement = '\n'.join(current_batch)
                        
                        # Write the input SQL to output file
                        outfile.write("Input SQL:\n")
                        outfile.write("-"*11 + "\n")
                        outfile.write(sql_statement + "\n\n")
                        
                        try:
                            # Execute the SQL batch
                            results = cursor.execute(sql_statement)
                            
                            # Write results header
                            outfile.write("Results:\n")
                            outfile.write("-"*8 + "\n")
                            
                            # Fetch and write column names if available
                            if cursor.description:
                                columns = [column[0] for column in cursor.description]
                                outfile.write(" | ".join(columns) + "\n")
                                outfile.write("-" * (len(" | ".join(columns))) + "\n")
                                
                                # Fetch and write results
                                for row in results:
                                    outfile.write(" | ".join(str(value) for value in row) + "\n")
                            
                            conn.commit()
                            
                        except pyodbc.Error as e:
                            # Seems to return error 'Protocol error in TDS stream' if the object exists
                            # so ignoring
                            if  e.args[0] == 'HY000' and 'Protocol error in TDS stream' in str(e):
                                outfile.write(f"Ignoring TDS Protocol error: {str(e)}\n")
                                outfile.write("Possibly caused by object already existing\n")
                                pass
                            else:
                                outfile.write(f"Error executing SQL: {str(e)}\n")
                                conn.rollback()
                            
                        outfile.write("\n" + "="*100 + "\n\n")
                        current_batch = []
                else:
                    current_batch.append(line)
                    
    except pyodbc.Error as e:
        print(f"Database connection error: {str(e)}")
    finally:
        if 'conn' in locals():
            conn.close()

def main():
    # Configuration
    input_file = "input.sql"
    output_file = "sql_results.txt"
    server = "my_server"
    user = "my_user"
    password = "my_password"
    
    execute_sql_file(input_file, output_file, server, user, password)
    print("SQL execution completed. Check output file for results.")

if __name__ == "__main__":
    main()

Successful execution

SQL Execution Results - 2025-01-29 20:02:42
--------------------------------------------------------------------------------

Input SQL:
-----------
USE [dcdb01]

Results:
--------

=====================================================================================

Input SQL:
-----------

use [dcdb01]

Results:
--------

=====================================================================================

Input SQL:
-----------
CREATE TABLE [dbo].[dc_test_01](
	[id_01] [bigint] NOT NULL,
	[data_01] [varchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
 CONSTRAINT [id_01] PRIMARY KEY CLUSTERED 
(
	[id_01] ASC
) 
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Results:
--------

=====================================================================================

Input SQL:
-----------
CREATE TABLE [dbo].[dc_test_02](
	[id_02] [bigint] NOT NULL,
	[data_02] [varchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
 CONSTRAINT [id_02] PRIMARY KEY CLUSTERED 
(
	[id_02] ASC
) 
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Results:
--------

=====================================================================================

Unsuccessful execution

SQL Execution Results - 2025-01-29 20:01:38
--------------------------------------------------------------------------------

Input SQL:
-----------
USE [dcdb01]

Results:
--------

=====================================================================================

Input SQL:
-----------

use [dcdb01]

Results:
--------

=====================================================================================

Input SQL:
-----------
CREATE TABLE [dbo].[dc_test_01](
	[id_01] [bigint] NOT NULL,
	[data_01] [varchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
 CONSTRAINT [id_01] PRIMARY KEY CLUSTERED 
(
	[id_01] ASC
) 
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Ignoring TDS Protocol error: ('HY000', '[HY000] [Microsoft][ODBC SQL Server Driver]Protocol error in TDS stream (0) (SQLExecDirectW)')
Possibly caused by object already existing

=====================================================================================

Input SQL:
-----------
CREATE TABLE [dbo].[dc_test_02](
	[id_02] [bigint] NOT NULL,
	[data_02] [varchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
 CONSTRAINT [id_02] PRIMARY KEY CLUSTERED 
(
	[id_02] ASC
) 
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Ignoring TDS Protocol error: ('HY000', '[HY000] [Microsoft][ODBC SQL Server Driver]Protocol error in TDS stream (0) (SQLExecDirectW)')
Possibly caused by object already existing

=====================================================================================
142
51
72
76