Thursday, April 10, 2008

Execute SQLCMD scripts via a class in C#

Here's a utility class I put together so I could take the output scripts from a Visual Studio 2008 database project and execute them from code without calling SQLCMD.EXE. The obstacle to direct execution of the scripts is they contain many instances of syntax only available through SQLCMD. To my knowledge there's no direct ADO.NET way to use SQLCMD syntax in scripts.

Note: You may even find this useful for any SQL Scripts simply because it supports the GO statement that typical ADO.Net does not.

My class supports several of the most import SQLCMD Syntax bits and gracefully ignores the one's it doesn't support. It's not a perfect replacement for SQLCMD but gets the job done. This class has not been extensively tested.

using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using System.Net;

namespace MyNameSpace
{


/// <summary>
/// Supports running a SQLCmd Mode style statement such as the output from a VS 2008 Database Team Edition Database Project
/// Only a limited subset of the SQLCmd mode syntax is supported. Other gotchas.
///
/// Uses a database transaction; if anything fails inside a On Error Exit group then the entire script is rolled back
///
///
/// Supported Commands:
/// GO (note GO [N] will only be executed once not N times)
/// :setvar
/// $(MyVar)
/// :on error exit
/// :on error resume (only for SQL Errors)
/// :on error ignore (only for SQL Errors)
///
/// The Following SQLCMD Commands are recognized but ignored. They will not crash your script if encountered but will be skipped
/// :ED
/// :Error
/// :!!
/// :Perftrace
/// :Quit
/// :Exit
/// :Help
/// :XML
/// :r
/// :ServerList
/// :Listvar
///
/// The following SQLCMD pre-defined variables are pre-defined by this class just like they are by SQLCMD
/// The only difference is SQLCMD actually used and/or updated these variable. This class simply has them predefined
/// with much the same values as SQLCMD did. The class allows you to change ALL variables (unlike SQLCMD) where some are
/// read only.
/// SQLCMDUSER ""
/// SQLCMDPASSWORD
/// SQLCMDSERVER {Server Name}
/// SQLCMDWORKSTATION {Computer Name}
/// SQLCMDLOGINTIMEOUT {Connection Timeout}
/// SQLCMDDBNAME {Database Name}
/// SQLCMDHEADERS "0"
/// SQLCMDCOLSEP " "
/// SQLCMDCOLWIDTH "0"
/// SQLCMDPACKETSIZE "4096"
/// SQLCMDERRORLEVEL "0"
/// SQLCMDMAXVARTYPEWIDTH "256"
/// SQLCMDMAXFIXEDTYPEWIDTH "0"
/// SQLCMDEDITOR "edit.com"
/// SQLCMDINI ""
///
/// The following pre-defnined variables ARE used by the class and thier values when set are not ignored
/// SQLCMDSTATTIMEOUT "0"
///
/// One Additional Variable is defined so that scripts could potentially detect they are running in this class instead
/// of SQLCmd.
/// SQLCMDREAL "0"
/// </summary>
public class ExecuteSQLCmdMode
{
private Dictionary<string, string> _variables; // Hidden
private List<string> _lockedVariables; // Hidden
private errorMode _errorMode; // Hidden
private SqlConnection _sqlConn; // Hidden
private List<string> _ignoredCommands; // Hidden
private bool _allowVariableOverwrites;

/// <summary>
/// Initializes a new instance of the <see cref="ExecuteSQLCmdMode"/> class.
/// </summary>
/// <param name="sqlConn">The SQL conn.</param>
public ExecuteSQLCmdMode(SqlConnection sqlConn)
{
// Check for legal values
if (sqlConn == null)
throw new Exception("sqlConn cannot be null");

// Set connection variable from supplied SQLConnection
_sqlConn = sqlConn;

// Load up the script variables
_variables = new Dictionary<string, string>();
_variables.Add(
"SQLCMDUSER", "");
_variables.Add(
"SQLCMDPASSWORD", "");
_variables.Add(
"SQLCMDSERVER", sqlConn.DataSource);
_variables.Add(
"SQLCMDWORKSTATION", sqlConn.WorkstationId);
_variables.Add(
"SQLCMDDBNAME", sqlConn.Database);
_variables.Add(
"SQLCMDLOGINTIMEOUT", sqlConn.ConnectionTimeout.ToString());
_variables.Add(
"SQLCMDSTATTIMEOUT", "0");
_variables.Add(
"SQLCMDHEADERS", "0");
_variables.Add(
"SQLCMDCOLSEP", "");
_variables.Add(
"SQLCMDCOLWIDTH", "0");
_variables.Add(
"SQLCMDPACKETSIZE", "4096");
_variables.Add(
"SQLCMDERRORLEVEL", "0");
_variables.Add(
"SQLCMDMAXVARTYPEWIDTH", "256");
_variables.Add(
"SQLCMDMAXFIXEDTYPEWIDTH", "0");
_variables.Add(
"SQLCMDEDITOR", "edit.com");
_variables.Add(
"SQLCMDINI", "");
_variables.Add(
"SQLCMDREAL", "0");

// Setup pre-locked variables
_lockedVariables = new List<string>();
_lockedVariables.Add(
"SQLCMDREAL");

// Setup the list of commands to be ignored
_ignoredCommands = new List<string>();
_ignoredCommands.Add(
":ED");
_ignoredCommands.Add(
":ERROR");
_ignoredCommands.Add(
":!!");
_ignoredCommands.Add(
":PERFTRACE");
_ignoredCommands.Add(
":QUIT");
_ignoredCommands.Add(
":EXIT");
_ignoredCommands.Add(
":HELP");
_ignoredCommands.Add(
":XML");
_ignoredCommands.Add(
":R");
_ignoredCommands.Add(
":SERVERLIST");
_ignoredCommands.Add(
":LISTVAR");

// Some other misc values
_errorMode = errorMode.errExit;
_allowVariableOverwrites =
false;

}

/// <summary>
/// Gets or sets a value indicating whether to allow variable overwrites.
/// If True then even though a variable is specified externally it may
/// be overwritten by :SetVar in the script. If False then the reverse
/// variables specified externally superscede :setvar
/// Default = false
/// </summary>
/// <value>true if allow variable overwrites; otherwise, false.</value>
public bool AllowVariableOverwrites
{
get { return _allowVariableOverwrites; }
set { _allowVariableOverwrites = value; }
}

/// <summary>
/// Sets a variable in advance of script execution.
/// </summary>
/// <param name="variableName">Name of the variable.</param>
/// <param name="variableValue">The variable value.</param>
public void SetVariable(string variableName, string variableValue)
{
variableName = variableName.Trim().ToUpper();
if (variableName.Length == 0 variableName.Contains(" "))
throw new Exception("Variable name cannot be blank or contain spaces");

// See if we already have this variable
if (_variables.ContainsKey(variableName))
_variables[variableName] = variableValue;
else
{
_variables.Add(variableName, variableValue);

if (!_allowVariableOverwrites)
_lockedVariables.Add(variableName);

}

}

/// <summary>
/// Executes the specified SQL script.
/// </summary>
/// <param name="scriptToExecute">The SQL script.</param>
public List<Exception> Execute(string scriptToExecute)
{
List<Exception> exceptions = new List<Exception>();
StringBuilder sqlCmd = new StringBuilder();
int begPos;
int endPos;
string temp;
string[] SQLScript;
SqlTransaction trx = null;

_sqlConn.Open();
trx = _sqlConn.BeginTransaction(System.Data.
IsolationLevel.ReadCommitted);

try
{
SQLScript = (scriptToExecute.Replace(
Environment.NewLine, "\n") + "\nGO\n").Split('\n');

// Loop each line in the script
for (int i = 0; i < SQLScript.GetUpperBound(0); i++)
{
// Prepare a specially modified version of the line for checking for commands
string ucaseLine = SQLScript[i].Replace("\t", " ").Trim().ToUpper() + " ";

// See if it's one of the commands to be ignored
if (!_ignoredCommands.Contains((ucaseLine).Split(' ')[0]))
{
// See if we have a GO line (everything after GO on the line is ignored)
if (ucaseLine.StartsWith("GO "))
{
try
{
if (sqlCmd.Length > 0)
{
// Attempt the SQL command
SqlCommand sqlComm = new SqlCommand(sqlCmd.ToString(), _sqlConn, trx);
sqlComm.ExecuteNonQuery();
}
}
catch (Exception ex)
{
if (_errorMode != errorMode.errIgnore)
throw new Exception("Error executing " + sqlCmd.ToString(), ex);
else
exceptions.Add(new Exception("Error executing " + sqlCmd.ToString(), ex));
}

// Reset the SQL Command
sqlCmd = new StringBuilder();

}

// Handle :SetVar MyVar "MyValue"
else if (ucaseLine.StartsWith(":SETVAR "))
{
temp = SQLScript[i].Trim().Substring(8, SQLScript[i].Trim().Length - 8);
begPos = temp.IndexOf(
" ");

string varName = temp.Substring(0, begPos).Trim().ToUpper();
string varValue = temp.Substring(begPos + 1, temp.Length - begPos - 1).Trim();
if (varValue.StartsWith("\"") && varValue.EndsWith("\""))
varValue = varValue.Substring(1, varValue.Length - 2);
else
throw new Exception(string.Format("Improperly formatted :SetVar on line {0}.", i));

if (_variables.ContainsKey(varName))
{
if (!_lockedVariables.Contains(varName))
_variables[varName] = varValue;
}
else
_variables.Add(varName, varValue);

}

// Handle :on error
else if (ucaseLine.StartsWith(":ON ERROR "))
{
temp = ucaseLine.Substring(10, ucaseLine.Length - 10).Trim();
if (temp == "EXIT")
_errorMode =
errorMode.errExit;
else if (temp == "RESUME" temp == "IGNORE")
_errorMode =
errorMode.errIgnore;
else
throw new Exception(string.Format("Unknown On Error mode '{0}' on line {1}", temp, i));
}

// Regular SQL Line to have variables replaced on then added to SQLCmd for execution
else
{
temp = SQLScript[i];

// Quick check to see if there's any possibility of variables in the line of SQL
if (temp.Length > 4 && temp.Contains("$("))
{
// Loop each variable to check the line for
foreach (KeyValuePair<string, string> keyPair in _variables)
{
string SearchFor = string.Format("$({0})", keyPair.Key);
begPos = temp.ToUpper().IndexOf(SearchFor);
while (begPos >= 0)
{
// Make the variable substitution
endPos = begPos + SearchFor.Length;
temp = temp.Substring(0, begPos) + keyPair.Value + temp.Substring(endPos, temp.Length - endPos);

// Calculate a new begPos
begPos = temp.ToUpper().IndexOf(string.Format(SearchFor));
}
}
}

sqlCmd.AppendLine(temp);
}

}
// If !IgnoreCommand
} // for ...

// All is well so commit the transaction
trx.Commit();

}
// Try
catch (Exception ex)
{
// Rollback transaction
if (trx != null)
trx.Rollback();

throw ex;


}

return exceptions;

}
// Execute
} // Class

/// <summary>
/// Legal values for the error mode
/// Error mode controls what happens when a SQL Error occurs
/// </summary>
public enum errorMode
{
errExit,
errIgnore
}
}

11 Comments:

Anonymous Anonymous said...

Needed a way to get rid of SQLCMD. That works really well! Thanks.

12:13 AM  
Anonymous Anonymous said...

Hi there Dear, are you actually visiting this web site regularly, if so after that
you will without doubt take good knowledge.

Feel free to visit my webpage - A few invisible details of moobs

8:17 AM  
Anonymous Anonymous said...

Fantastic website. A lot of useful information here. I am sending it to several pals ans also sharing in delicious.
And obviously, thank you on your effort!

Also visit my website ... chestfatburner.com

8:37 AM  
Anonymous Anonymous said...

You actually make it seem so easy along with your presentation but I to find
this matter to be really one thing that I believe I might never understand.
It seems too complex and very broad for me. I am
taking a look ahead on your next put up, I will try to get the dangle of it!


My homepage: chestfatburner.com

7:08 AM  
Anonymous Anonymous said...

Greetings, I do believe your site could possibly be having browser compatibility problems.
When I take a look at your site in Safari, it looks fine however,
when opening in I.E., it has some overlapping issues. I simply wanted to provide
you with a quick heads up! Apart from that, excellent website!



Feel free to surf to my web-site How to Increase Your Androgen hormone or the male growth hormone Stage to reduce Moobs

7:24 AM  
Anonymous Anonymous said...

I am curious to find out what blog platform you're using? I'm experiencing some minor security issues with my latest blog and I would like to find something
more secure. Do you have any suggestions?

Look into my blog ... Will be Gynexin Successful In opposition to Moobs?

8:25 AM  
Anonymous Anonymous said...

I am sure this piece of writing has touched all the internet viewers, its really really
nice article on building up new web site.

Feel free to surf to my web blog :: Use of turmeric root extract regarding gynecomastiatherapy

8:03 PM  
Anonymous Anonymous said...

I am curious to find out what blog platform you have been using?
I'm experiencing some minor security issues with my latest site and I'd like to find something more safeguarded.
Do you have any solutions?

Also visit my blog post ... chestfatburner.com

10:06 PM  
Anonymous Anonymous said...

I like looking through an article that will make men and women think.
Also, many thanks for allowing me to comment!



Also visit my web blog :: How to build the V Form body ?

10:13 PM  
Anonymous Anonymous said...

This is my first time pay a quick visit at here and i am
genuinely happy to read everthing at one place.


Here is my web site :: Using gynexin regarding normal treatment of gynecomastia

10:13 PM  
Anonymous Anonymous said...

Yes! Finally something about acdc tnt.

My web page Low surgical procedure selections for men breasts therapy

10:19 PM  

Post a Comment

<< Home