Click here to Skip to main content
15,895,256 members
Articles / Database Development / SQL Server / SQL Server 2008

Continuous Integration for Databases with Visual Studio

Rate me:
Please Sign up or sign in to vote.
4.85/5 (14 votes)
23 Nov 2010Apache18 min read 63.3K   1.2K   68  
Provides a framework for predictably compiling, extracting, and deploying a database project.
// <copyright file="ScriptExecutor.cs" company="Adam Nachman">
// Copyright (c) 2009 All Right Reserved Adam Nachman
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
// <author>Adam Nachman</author>
namespace SqlDeployment
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using Microsoft.SqlServer.Management.Common;
    using Microsoft.SqlServer.Management.Smo;

    public class ScriptExecutor
    {
        #region Constants

        private const string DatabaseReplacementToken = "$(DatabaseName)";

        #endregion Constants

        #region Private Variables

        private Microsoft.SqlServer.Management.Smo.Server server = null;
        private Microsoft.SqlServer.Management.Smo.Database database = null;
        private ScriptExecutorConfiguration config = null;
        private volatile object syncRoot = new object();
        private bool stopping = false;
        private string compoundFile;
        private bool outputCompoundFile = false;

        #endregion Private Variables

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the ScriptExecutor class
        /// </summary>
        /// <param name="server">The server to which the executor must connect</param>
        /// <param name="database">The database against which the scripts must be executed</param>
        /// <param name="config">The configuratoin</param>
        public ScriptExecutor(Server server, Database database, ScriptExecutorConfiguration config)
        {
            if (server == null)
            {
                throw new ArgumentNullException("server", "Server cannot be null");
            }

            if (database == null)
            {
                throw new ArgumentNullException("database", "Database cannot be null");
            }

            if (config == null)
            {
                throw new ArgumentNullException("config", "Config cannot be null");
            }

            if (config.ExecutionSequence.Count < 1)
            {
                throw new ArgumentOutOfRangeException("config", "Connfig.ExecutionSequence cannot be empty. At least one script needs to be defined");
            }

            this.server = server;
            this.database = database;
            this.config = config;
        }

        #endregion Constructors

        #region Public Delegates

        public delegate void ScriptExecutorEventHandler(object sender, ScriptExecutorEventArgs[] e);

        #endregion Public Delegates

        #region Public Events

        public event ScriptExecutorEventHandler Progress;

        #endregion Public Events

        #region Public Properties

        /// <summary>
        /// Sets the path of the output file in the event that a single coumpound file is required
        /// </summary>
        public string OutputCompoundFilePath
        {
            set
            {
                this.compoundFile = value;
            }
        }

        /// <summary>
        /// Sets a value indicating whether or not a single output file is required
        /// </summary>
        public bool OutputCompoundFile
        {
            set
            {
                this.outputCompoundFile = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether or not a using statement should be created at the beginning of the output file
        /// </summary>
        /// <value><c>true</c> if a using statement should be created at the beginning of the output file; otherwise, <c>false</c>.
        /// </value>
        public bool PrependUsingDatabaseName { get; set; }

        #endregion Public Properies

        #region Public Methods

        /// <summary>
        /// Executes the scripts
        /// </summary>
        /// <returns>false if usuccessful</returns>
        public bool Execute()
        {
            if (this.outputCompoundFile)
            {
                FileBuilder.SetPath(this.compoundFile);
                FileBuilder.Initialize();
            }

            this.ValidateConfiguration();
            this.server.ConnectionContext.InfoMessage += new System.Data.SqlClient.SqlInfoMessageEventHandler(this.ConnectionContext_InfoMessage);

            lock (this.syncRoot)
            {
                this.stopping = false;
                var sortedList = from item in this.config.ExecutionSequence orderby item.Key select item;

                if (this.stopping)
                {
                    this.RaiseStoppedEvent();
                    return false;
                }

                if (this.PrependUsingDatabaseName && this.outputCompoundFile)
                {
                    FileBuilder.AddText(string.Format("use [{0}]", this.database.Name));
                    FileBuilder.AddText("GO");
                }

                foreach (KeyValuePair<int, string> kvp in sortedList)
                {
                    if (this.stopping)
                    {
                        this.RaiseStoppedEvent();
                        return false;
                    }

                    this.OnProgress(new ScriptExecutorEventArgs[] { new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.ScriptExecution, kvp.Value) });
                    this.ExecuteScriptFile(kvp.Value);
                }

                this.OnProgress(new ScriptExecutorEventArgs[] { new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.ExecutionComplete, string.Empty) });

                if (this.outputCompoundFile)
                {
                    FileBuilder.Flush();
                }
                return true;
            }
        }

        public void Stop()
        {
            this.stopping = true;
            this.OnProgress(new ScriptExecutorEventArgs[] { new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.ExecutionStoppingByRequest, "Stopping Execution") });
        }

        #endregion Public Methods

        #region Protected Methods

        protected virtual void OnProgress(ScriptExecutorEventArgs[] e)
        {
            if (this.Progress != null)
            {
                this.Progress(this, e);
            }
        }

        #endregion Protected Methods

        #region Private Methods

        private void RaiseStoppedEvent()
        {
            this.OnProgress(new ScriptExecutorEventArgs[] { new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.ExecutionStoppedByRequest, "Script execution stopped") });
        }

        /// <summary>
        /// Executes the script file.
        /// </summary>
        /// <param name="filename">The filename.</param>
        private void ExecuteScriptFile(string filename)
        {
            string sqlCommand = string.Empty;
            using (StreamReader streamReader = new StreamReader(filename))
            {
                sqlCommand = streamReader.ReadToEnd();
                streamReader.Close();
            }

            this.database.ExecuteNonQuery(sqlCommand.Replace(DatabaseReplacementToken, this.database.Name), ExecutionTypes.Default);
            if (this.outputCompoundFile)
            {
                FileBuilder.AddText(sqlCommand.Replace(DatabaseReplacementToken, this.database.Name));
            }
        }

        #endregion Private Methods

        #region Private Event Handlers

        private void ValidateConfiguration()
        {
            if (!string.IsNullOrEmpty(this.config.TargetServer) && this.server.Name.ToUpperInvariant() != this.config.TargetServer.ToUpperInvariant())
            {
                throw new Exception(string.Format("You may only deploy this update against the {0} server, and not {1}", this.config.TargetServer, this.server.Name));
            }

            if (!string.IsNullOrEmpty(this.config.TargetDatabase) && this.database.Name.ToUpperInvariant() != this.config.TargetDatabase.ToUpperInvariant())
            {
                throw new Exception(string.Format("You may only deploy this update against the {0} database, and not {1}", this.config.TargetDatabase, this.database.Name));
            }
        }

        private void ConnectionContext_InfoMessage(object sender, System.Data.SqlClient.SqlInfoMessageEventArgs e)
        {
            if (e.Errors != null && e.Errors.Count > 0)
            {
                int count = e.Errors.Count;
                ScriptExecutorEventArgs[] args = new ScriptExecutorEventArgs[e.Errors.Count];
                for (int i = 0; i < count; i++)
                {
                    switch (e.Errors[i].Number)
                    {
                        case 100000:
                            args[i] = new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.ObjectCount, e.Errors[i].Message);
                            break;
                        case 100010:
                            args[i] = new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.Progress, e.Errors[i].Message);
                            break;
                        default:
                            args[i] = new ScriptExecutorEventArgs(ScriptExecutorEventArgs.EventMessageCode.SqlInfoMessage, e.Errors[i].Message);
                            break;
                    }
                }

                this.OnProgress(args);
            }
            else
            {
                Debug.Print("Huh " + e.Message);
            }
        }

        #endregion Private Event Handlers

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Chief Technology Officer
Australia Australia
Emigrated to Sydney, Australia in 2013 from Cape Town, South Africa, and have been writing commercial software since 1997.

Expertise includes MS SQL Server (7 till latest), C#, VB6, VB.NET, VBScript, JavaScript, ASP, HTML, WPF Angular, Windows Installer and InstallShield (multiple versions) and a partridge in a pear tree. MSBuild, CruiseControl.NET, TFS, Jenkins, TeamCity, ant and nant are all necessary sidelines. Have tinkered with Java and C++

Experienced with Enterprise level application design and deployment, as well as sizing and scaling high volume OLTP database designs up to tens of thousands of transactions per second and diagnosing application and database performance bottlenecks.

Comments and Discussions