Click here to Skip to main content
Click here to Skip to main content

A Tool to Compare SQL Database Schema Versions

, 26 Feb 2009
Rate this:
Please Sign up or sign in to vote.
An article about a tool for comparing SQL database schema versions
SchemaToolUse.jpg

Introduction

The challenge of developing code for any small or large development project that uses a SQL database, is that often the code and database schema change or get modified over the duration of the SDLC from what was previously delivered to the customer.

It is not always easy to ensure that the previously delivered schema stays in sync with the code unless rigorous project process controls are in place. I have worked on several projects, and every one had a different approach to ensuring that code and database schema stay in sync.

This tool which I call the SQL Schema Tool or SST, was created to assist in handling the problems of comparing or providing update scripts that work against the issue of synchronizing a customer database schema from a previous version when migrating to a new version.

One way that we can deliver schema changes for an existing database, is to create a new database with the updated schema and then use some ETL process to migrate data from the old database into the new one. However my experience with customers is that they are reluctant to have to swap out a production database for an entirely new one.

My approach to the problem using SST, was to provide a way to capture the delivered customer's schema as an XML snapshot that describes the schema. This snapshot of the customer's database schema can then be compared to the current development database schema without requiring active connections and provide an update SQL script to run against the customer database.

Current Features

  • Schema XML snapshots, for portable SQL schema generation/compare without a database
  • Schema generation – with selectable object types
  • Schema compare – with selectable object types
  • DTS package XML Snapshots, for creating portable DTS packages
  • Two Command-line tools for the above listed functions. This should allow ant build or other automation tools to compare or generate schema updates automatically.
  • Data compare – currently of individual tables only
  • Data export – CVS and XLS, plan to add text file output for SQL insert statements
  • Reports for data compare, Schema; tables, views, functions, and sprocs
  • XML tree browser with XPath search of XML snapshots generated by the tools
  • Run generated SQL against selected DB without switching to yet another tool
  • Logging of all events, actions and errors - both SQL related and application type errors.
  • Custom User Defined XSLT transformations can be automatically applied to the generated SQL Schema and Data XML output.

Background

I worked on the QueryCommander SQL Editor project at one point. Some of the design approaches used in SST come from my experiences with working in code base.

The SQL Schema Tool or SST started life as a command line project to get DTS packages out of the database as XML. Additional code was added later to get schema changes made against development databases that did not exist in the QA databases and create update scripts to run against QA databases during the continuous build process.

The schema tool was then turned into a common set of classes that could be called from either a command line or a Winform GUI application. I had decided to try and sell the software, but never seemed to generate much interest. Therefore, I am writing this article and giving my code away under the attached license as well as any license used by the libraries contained within the code.

Using the Code

The code was written and tested against Microsoft SQL Server 2000 and SQL Server 2005. I have separated the classes such that it should be an easy modification to add support for other vendor databases, such as MYSQL or DB2, Oracle; although the DTS Package classes would only work with SQL 2000.

Architecture of the schema module has been designed to deal with the database at the object level. This allows individual groups of objects to be selected for the compare or generate schema operations. It also allows the user of the application to narrow their focus to the exact objects of a database that are in flux, or changing often.

When you examine the output of the XML snapshot, you can see this approach by noting the XML nodes or elements are grouped by object type, and sorted by object name.

All principal output from the schema module is in the form of XML, that is in turn transformed into the appropriate output, by the use of XSLT and XML transformations.

The SQL Schema Tool is designed to consider a master or source database, which is the current or latest database schema for use in a project. The destination or target database is considered by SST to be the older schema, or the schema that needs updating.

Major Design Considerations

  • Keep intact any data that exists in the target database
  • Ability to make schema comparisons in a fully or partially disconnected mode by the use of saving a XML schema snapshot for the source database, target database or both
  • Use XSLT to transform XML into appropriate objects

Shown below is a simplified sequence diagram of the SQLSchemaTool class.

SSTSequenceDiagram.jpg

The SQLSchemaTool class uses the SQLObjects namespace and the classes: Tables, Views, Sprocs, etc. that serialize the schema of those objects as XML. The code for the objects in the database makes use of Generics where appropriate.

public static string SerializeDB(
    string SQLServer,
    string DBName,
    string UID,
    string PWD,
    string SQLfile,
    bool Translate,
    bool Primary,
    object threaded,
    byte objectsToSerialize,
    string CustomXSLT,
    string delimTableNames)
{
    _threaded = threaded;
    string _serverDB = SQLServer + ":" + DBName;
    string outputFile = string.Format
		(_OUTPUTFILE, SQLServer.Replace("\\", "_").Replace(":", "-"),
                DBName.Replace("\\", "_").Replace(":", "-"));
    try
    {
        // TODO:  add threads if this takes a long while
        SQLMethods.SQLConnections _connections;
        if (UID != null && PWD != null)
        {
            _connections = new SQLMethods.SQLConnections
				(SQLServer, DBName, UID, PWD, false);
        }
        else
        {
            _connections = new SQLMethods.SQLConnections(SQLServer, DBName);
        }
        if (_connections != null && _connections.Count > 0)
        {
            DataSet _ds = new DataSet("DataBase_Schema");
            _ds.EnforceConstraints = false;
            DataTable dt = _ds.Tables.Add("Database");
            dt.Columns.Add("Name");
            dt.Columns.Add("Date");
            dt.Columns.Add("Time");
            DataRow dr = dt.NewRow();
            dr.ItemArray =
                 new object[] { DBName, DateTime.Now.ToShortDateString(),
			DateTime.Now.ToShortTimeString() };
            dt.Rows.Add(dr);

            Sleep();

            // get defaults, rules and UDDTs :
		    // in this order because of dependant behavior
            if ((objectsToSerialize & Convert.ToByte(_NodeType.DEFAULT)) ==
			(int)_NodeType.DEFAULT)
            {
                SQLObjects.Defaults _defaults = new SQLObjects.Defaults(DBName);
                _defaults.GetObject<sqlobjects.defaults>(_connections[0].sqlConnection);

                _ds.Merge(_defaults);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.RULE)) ==
			(int)_NodeType.RULE)
            {
                SQLObjects.Rules _rules = new SQLObjects.Rules(DBName);
                _rules.GetObject<sqlobjects.rules>(_connections[0].sqlConnection);

                _ds.Merge(_rules);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.UDDT)) ==
			(int)_NodeType.UDDT)
            {
                SQLObjects.UDDTs _uddts = new SQLObjects.UDDTs(DBName);
                _uddts.GetObject<sqlobjects.uddts>(_connections[0].sqlConnection);

                _ds.Merge(_uddts);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.TABLE)) ==
			(int)_NodeType.TABLE)
            {
                SQLObjects.Tables _tables = new SQLObjects.Tables(DBName);
                if (!string.IsNullOrEmpty(delimTableNames))
                {
                    _tables.GetObject<sqlobjects.tables>(_connections[0].sqlConnection,
						Primary, delimTableNames);
                }
                else
                {
                    _tables.GetObject<sqlobjects.tables>
			(_connections[0].sqlConnection, Primary);
                }
                // TODO:  make work with DBs attached as MDF files to SQL 2005
                _ds.Merge(_tables);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.VIEW)) ==
			(int)_NodeType.VIEW)
            {
                SQLObjects.Views _views = new SQLObjects.Views(DBName);
                _views.GetObject<sqlobjects.views>(_connections[0].sqlConnection);

                _ds.Merge(_views);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.SPROC)) ==
			(int)_NodeType.SPROC)
            {
                SQLObjects.Sprocs _sprocs = new SQLObjects.Sprocs(DBName);
                _sprocs.GetObject<sqlobjects.sprocs>(_connections[0].sqlConnection);

                _ds.Merge(_sprocs);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.FUNCTION)) ==
			(int)_NodeType.FUNCTION)
            {
                SQLObjects.Funcs _funcs = new SQLObjects.Funcs(DBName);
                _funcs.GetObject<sqlobjects.funcs>(_connections[0].sqlConnection);

                _ds.Merge(_funcs);
            }
            Sleep();

            if ((objectsToSerialize & Convert.ToByte(_NodeType.TRIGGER)) ==
			(int)_NodeType.TRIGGER)
            {
                SQLObjects.Triggers _triggers = new SQLObjects.Triggers(DBName);
                _triggers.GetObject<sqlobjects.triggers>(_connections[0].sqlConnection);

                _ds.Merge(_triggers);
            }
            // TODO: add jobs, users, roles

            Sleep();

            // get rid of old files
            if (File.Exists(outputFile)) File.Delete(outputFile);

            // write out xml schema document
            XmlDataDocument xmlData = new XmlDataDocument(_ds);
            //xmlData.Save(outputFile);

            // reload to xml schema to avoid the "deleted row" error
		    // when removing the dependant child nodes
            XmlDocument xmlDoc = new XmlDocument();
            //xmlDoc.Load(outputFile);
            xmlDoc.LoadXml(xmlData.OuterXml);

            Sleep();

            // sort the dependencies for views, functions, and stored procedures
            SortDependencies(_serverDB, VIEWPATH, VIEWDEP, ref xmlDoc);
            SortDependencies(_serverDB, FUNCPATH, FUNCDEP, ref xmlDoc);
            SortDependencies(_serverDB, SPROCPATH, SPROCDEP, ref xmlDoc);

            foreach (Char c in Path.GetInvalidFileNameChars())
            {
                outputFile = outputFile.Replace(c, '_');
            }
            foreach (Char c in Path.GetInvalidPathChars())
            {
                outputFile = outputFile.Replace(c, '_');
            }
            xmlDoc.Save(outputFile);

            // perform garbage collection to free up memory
            GC.Collect();

            if (Translate && outputFile != null && outputFile.Trim().Length > 0)
            {
                string createName = outputFile.ToLower().Replace(".xml", ".sql");
                if (SQLfile != null && SQLfile.Length > 0)
                {
                    createName = SQLfile.ToLower().Replace(".xml", ".sql");
                }
                if (!createName.EndsWith(".sql")) { createName += ".sql"; }
                XsltHelper.SQLTransform(outputFile,
				XsltHelper.SQLCREATEXSLT, createName);
                outputFile += "," + createName;
                logger.Info("\nSQL Create Schema has been saved to " +
					createName + ".");
            }
            if (CustomXSLT != null && CustomXSLT.Trim().Length > 0)
            {
                FileInfo fi = new FileInfo(CustomXSLT);
                File.WriteAllText("CustomOutput.XML",
			XsltHelper.Transform(xmlDoc.OuterXml, fi));
                logger.Info("\nThe Custom XSLT {0},
			has been applied and saved as
				CustomOutput.XML.", CustomXSLT);
            }
        }
    }
    catch (Exception ex)
    {
        if (ex is System.Data.SqlClient.SqlException)
        {
            logger.Error("\nSQL Error: {0}, DB Server {1}",
						ex.Message, _serverDB);
        }
        else
        {
            logger.Error(ERRORFORMAT, ex.Message, ex.Source, ex.StackTrace);
        }
    }
    return outputFile;
}

Once the objects of the database are serialized as XML, then the code can perform XSLT transformations against the XML to make SQL, or other output.

Additionally, if there are two such XML representations of databases, those serialized databases can be compared. The results of that comparison are output as XML which again can be transformed into the necessary SQL to update the target database.

SQLSchemaTool_Class.jpg

TablesClassDiagram.jpg

Points of Interest

Here is a screen shot of the XML output from the tool:

XMLDocument.jpg

Here is a screen shot of the SQL output from the tool after the XSLT transformation get applied:

SQLDocumentWindow.jpg

Here is a screen shot of the HTML difference report created from the tool after the XSLT transformation get applied:

sst_html_report.jpg

Solution Building

The build process uses the ILMerge to merge the project's DLLs together into a single assembly in the pre/post build commands. See Microsoft Research for the download of this tool. This makes the deployment installer much easier to design.

For the SQLSchemaToolGUI project, edit the pre and post build events which are accessed from the project properties.

There is an initial line for both pre and post build events that has:

del "$(TargetDir)merge.log"

That line should be changed to:

IF EXIST "$(TargetDir)merge.log" del "$(TargetDir)merge.log"

This is required because I first ran the merge.bat file from the commandline, which creates the merge.log file. So I never thought about the fact that the log file would not be there.

The log file is the output log of the .NET iLMerge tool, which the batch file is using to merge all the individual DLLs into the single SSTassemblies.dll file.

TODO

  • The tools need lots of testing, I'm sure there are still bugsSmile | :)
  • Improve SQL object dependency calculations and performance
  • Design XSLT to generate base data access classes using .NET - similar to MyGeneration tool
  • Create other database provider classes for MySql, DB2, and Oracle
  • Enhance Data Compare functionality

Credits

  • Weifen Luo - DockPanel Suite is designed to achieve docking capability for MDI forms. It can be used to develop Visual Studio .NET style applications.
  • #develop - SharpDevelop is an open source IDE for the .NET platform. SST uses ICSharpCode.TextEditor.
  • QueryCommander - The QueryCommander dev team for the ideas and approaches to work with SQL Server.
  • NLog - NLog is a .NET logging library designed with simplicity and flexibility in mind.
  • WIX - The Windows Installer XML (WiX) is a toolset that builds Windows installation packages from XML source code. The toolset supports a command line environment that developers may integrate into their build processes to build MSI and MSM setup packages.
  • XML Diff - The Microsoft XML Diff Tool.

More Information

History

  • January 2005 - Finished Command line tool
  • June 2007 - Finished GUI version
  • Sept 2007 - Started Beta tests
  • Current - Decided to give away source for free

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)

Share

About the Author

Lindsey Lewis
Web Developer LewisSoftware
United States United States
Software Developer since 1981 starting with dBase II.
Computer Instructor for secondary/post-secondary students from 1983-1988.
IT UNIX Admin/Foxpro Developer 1988-1991
Microsoft VS C++/VB/C#.NET Developer 1991 - present
Current: Manager of small developer team
 
I like small sports cars - have owned 5 Triumphs, and a couple of Fiat Spyders.

Comments and Discussions

 
GeneralTHANKS PinmemberMember 247570419-May-09 6:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140814.1 | Last Updated 26 Feb 2009
Article Copyright 2009 by Lindsey Lewis
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid