Click here to Skip to main content
15,896,493 members
Articles / Desktop Programming / Windows Forms

SQLite Compare Utility

Rate me:
Please Sign up or sign in to vote.
4.89/5 (68 votes)
21 Feb 2015LGPL35 min read 285.5K   37.2K   131  
Utility for comparing two SQLite database files for both structure and data
using System;
using System.Collections.Generic;
using System.Text;
using SQLiteParser;

namespace SQLiteTurbo
{
    /// <summary>
    /// This class is responsible to generate change scripts that can be 
    /// used to migrate from one version of a database to another.
    /// </summary>
    public class ChangeScriptBuilder
    {
        #region Constructors
        #endregion

        #region Public Methods
        /// <summary>
        /// Generate a change script needed to migrate from the left database to the right database object
        /// </summary>
        /// <returns></returns>
        public static string Generate(
            string leftdb,
            string rightdb,
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> leftSchema,
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> rightSchema,
            Dictionary<SchemaObject, List<SchemaComparisonItem>> comp, ChangeDirection direction)
        {
            StringBuilder sb = new StringBuilder();

            // Do everything within a transaction
            sb.Append("-- Generated by SQLite Compare utility\r\n\r\n");
            if (direction == ChangeDirection.LeftToRight)
                sb.Append("-- The script can be used to migrate database\r\n-- " + leftdb + " schema\r\n-- to the schema of database\r\n-- " + rightdb + "\r\n\r\n");
            else
                sb.Append("-- The script can be used to migrate database\r\n-- " + rightdb + " schema\r\n-- to the schema of database\r\n-- " + leftdb + "\r\n\r\n");
            sb.Append("BEGIN TRANSACTION;\r\n\r\n");

            // There are 4 different types of objects that can be migrated:
            // 1. Tables
            // 2. Indexes
            // 3. Triggers
            // 4. Views
            // Indexes depend on their tables so we can handle them only after handling the
            // respective tables. Triggers depend on the associated table or view. If a trigger
            // depends on a table - we can handle it only after handling the related table. If
            // a trigger depends on a view - we can handle it only after handling the view.
            // A view depends on one or more tables, but, unlike other objects - we can simply drop
            // it and re-create it from scratch without damaging any data or schema information.

            // We'll start by handling the hardest part first - tables
            List<SchemaComparisonItem> tables = comp[SchemaObject.Table];
            foreach (SchemaComparisonItem item in tables)
            {
                if (item.Result != ComparisonResult.Same)
                {
                    string script = GenerateTableChangeScript(item, comp, direction, leftSchema, rightSchema);
                    sb.Append(script);
                }
                else
                {
                    // The table itself did not change, but we still need to check its indexes and triggers
                    if (direction == ChangeDirection.LeftToRight)
                        ApplyIndexOrTriggerChanges(sb, item.LeftDdlStatement, leftSchema, rightSchema);
                    else
                        ApplyIndexOrTriggerChanges(sb, item.RightDdlStatement, rightSchema, leftSchema);
                } // else
            } // foreach

            // Note: as part of creating a change script for a table object - we'll also
            //       generate change script for its related indexes and triggers so they
            //       don't require a separate handling here.

            // We'll finish my handling views
            List<SchemaComparisonItem> views = comp[SchemaObject.View];
            foreach (SchemaComparisonItem item in views)
            {
                if (item.Result != ComparisonResult.Same)
                {
                    string script = GenerateViewChangeScript(item, direction);
                    sb.Append(script);
                }
                else
                {
                    // The view itself did not change, but we still need to check its triggers
                    if (direction == ChangeDirection.LeftToRight)
                        ApplyIndexOrTriggerChanges(sb, item.LeftDdlStatement, leftSchema, rightSchema);
                    else
                        ApplyIndexOrTriggerChanges(sb, item.RightDdlStatement, rightSchema, leftSchema);
                }
            } // foreachs

            // Commit the transaction
            sb.Append("\r\nCOMMIT TRANSACTION;\r\n");

            return sb.ToString();
        }
        #endregion

        #region Private Methods
        private static string GenerateViewChangeScript(SchemaComparisonItem item, 
            ChangeDirection direction)
        {
            StringBuilder sb = new StringBuilder();

            if (item.Result == ComparisonResult.ExistsInLeftDB)
            {
                if (direction == ChangeDirection.LeftToRight)
                    DeleteView(sb, item.LeftDdlStatement);
                else
                    CopyView(sb, item.LeftDdlStatement);
            }
            else if (item.Result == ComparisonResult.ExistsInRightDB)
            {
                if (direction == ChangeDirection.LeftToRight)
                    CopyView(sb, item.RightDdlStatement);
                else
                    DeleteView(sb, item.RightDdlStatement);
            }
            else if (item.Result == ComparisonResult.DifferentSchema)
            {
                if (direction == ChangeDirection.LeftToRight)
                    CopyView(sb, item.RightDdlStatement);
                else
                    CopyView(sb, item.LeftDdlStatement);
            } // else

            return sb.ToString();
        }

        private static void DeleteView(StringBuilder sb, SQLiteDdlStatement stmt)
        {
            sb.Append("\r\n-- Deleting view " + stmt.ObjectName.ToString() + " from the updated schema\r\n\r\n");
            sb.Append("DROP VIEW IF EXISTS " + stmt.ObjectName.ToString() + ";\r\n");
        }

        private static void CopyView(StringBuilder sb, SQLiteDdlStatement stmt)
        {
            sb.Append("\r\n-- Creating view " + stmt.ObjectName.ToString() + " from scratch\r\n\r\n");
            sb.Append("DROP VIEW IF EXISTS " + stmt.ObjectName.ToString() + ";\r\n");
            sb.Append(stmt.ToString() + ";\r\n");
        }

        private static string GenerateTableChangeScript(SchemaComparisonItem item,
            Dictionary<SchemaObject, List<SchemaComparisonItem>> comp, ChangeDirection direction,
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> leftSchema,
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> rightSchema)
        {
            StringBuilder sb = new StringBuilder();

            if (item.Result == ComparisonResult.ExistsInLeftDB)
            {
                if (direction == ChangeDirection.LeftToRight)
                    DeleteTable(sb, item.LeftDdlStatement);
                else
                    CopyTable(sb, item.LeftDdlStatement, leftSchema);                    
            }
            else if (item.Result == ComparisonResult.ExistsInRightDB)
            {
                if (direction == ChangeDirection.RightToLeft)
                    DeleteTable(sb, item.RightDdlStatement);
                else
                    CopyTable(sb, item.RightDdlStatement, rightSchema);
            }
            else if (item.Result == ComparisonResult.DifferentSchema)
            {
                if (direction == ChangeDirection.LeftToRight)
                    MigrateTable(sb, item.RightDdlStatement, leftSchema, rightSchema);
                else
                    MigrateTable(sb, item.LeftDdlStatement, rightSchema, leftSchema);
            }

            return sb.ToString();
        }

        /// <summary>
        /// Create the SQL code that is necessary to migrate an existing table to its
        /// updated schema.
        /// </summary>
        /// <param name="sb">The string builder to which the code will be added</param>
        /// <param name="stmt">The updated table schema object</param>
        /// <param name="srcSchema">The source schema</param>
        /// <param name="dstSchema">The destination schema</param>
        private static void MigrateTable(StringBuilder sb, SQLiteDdlStatement stmt, 
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> srcSchema, 
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> dstSchema)
        {
            List<SQLiteColumnStatement> added = null;
            SQLiteDdlStatement orig = srcSchema[SchemaObject.Table][SQLiteParser.Utils.Chop(stmt.ObjectName.ToString()).ToLower()];

            if (Utils.AlterTableIsPossible(orig, stmt, ref added))
            {
                sb.Append("\r\n-- Adding missing table columns\r\n\r\n");

                // In this case we can migrate the table by doing ALTER TABLE ADD COLUMN commands
                foreach (SQLiteColumnStatement col in added)
                {
                    sb.Append("ALTER TABLE " + stmt.ObjectName.ToString() + " ADD COLUMN " + col.ToString() + ";\r\n");
                } // foreach

                // If there are any differences in indexes or triggers - apply them now
                ApplyIndexOrTriggerChanges(sb, stmt, srcSchema, dstSchema);
            }
            else
            {
                // In this case we need to re-build the table from scratch in order to "change" it.
                sb.Append("\r\n-- Creating table "+stmt.ObjectName.ToString()+" from scratch (simple ALTER TABLE is not enough)\r\n\r\n");

                // Create a table with the correct schema but with a temporary name 
                string tmpname = Utils.GetTempName(stmt.ObjectName.ToString());
                SQLiteCreateTableStatement updTable = (SQLiteCreateTableStatement)stmt;
                sb.Append(updTable.ToStatement(tmpname) + ";\r\n");

                // Get the original table schema object
                SQLiteCreateTableStatement origTable = (SQLiteCreateTableStatement)orig;

                // Compute the set of common columns to the updated table schema and the original
                // table schema
                List<SQLiteColumnStatement> diffcols = null;
                List<SQLiteColumnStatement> common = Utils.GetCommonColumns(origTable, updTable, false, out diffcols);

                // Copy data rows from the original table to the temporary table
                if (common.Count > 0)
                {
                    // Check if all the columns that belong solely to the updated table schema (and are not
                    // common with the original table schema) supports NULL values or have DEFAULT values.
                    bool error = false;
                    List<SQLiteColumnStatement> ncols = new List<SQLiteColumnStatement>();
                    foreach (SQLiteColumnStatement c in updTable.Columns)
                    {
                        if (!Utils.ColumnListContains(common, c))
                            ncols.Add(c);
                    } // foreach
                    foreach (SQLiteColumnStatement c in ncols)
                    {
                        if (c.IsNullable || c.HasNonNullConstDefault)
                            continue;

                        // This column will cause any attempt to insert data into the updated table
                        // schema to fail, so we'll issue a warning comment
                        error = true;
                        sb.Append("\r\n-- WARNING: Column " + c.ObjectName.ToString() + " in table " + updTable.ObjectName.ToString() + " is NOT NULL and doesn't have a non-null constant DEFAULT clause. " +
                                  "\r\n--          This will cause any attempt to copy rows from the original table to the updated table to fail." +
                                  "\r\n--          No rows will be copied from the original table to the updated table!");
                        sb.Append("\r\n");
                    } // foreach

                    // Build a select columns list                    
                    string selectlist = Utils.BuildColumnsString(common, false);

                    // Build a select column list for those columns that belong exclusively to the updated table
                    // schema and that are nullable or have non-null DEFAULT clause.
                    string extralist = string.Empty;
                    if (ncols.Count > 0)
                        extralist = "," + Utils.BuildNullableOrNonNullConstantDefaultSelectList(ncols);

                    // Compute the list of all columns that are common to both tables and those that exist only in the
                    // updated table but are nullable or have non-null constant default.                    
                    string allCols = null;
                    if (common.Count > 0)
                    {
                        if (ncols.Count > 0)
                            allCols = selectlist + "," + Utils.BuildColumnsString(ncols, false);
                        else
                            allCols = selectlist;
                    }
                    else
                        allCols = Utils.BuildColumnsString(ncols, false);

                    if (!error)
                    {
                        // Now copy only the columns that can be transferred from the original table
                        sb.Append("\r\n-- Copying rows from original table to the new table\r\n\r\n");
                        sb.Append("INSERT INTO " + tmpname + " ("+allCols+")"+
                            " SELECT " + selectlist + extralist+" FROM " + origTable.ObjectName.ToString() + ";\r\n");
                    } // if
                } // if

                // Drop the original table and rename the temporary table to have the name of the original table
                sb.Append("\r\n-- Droping the original table and renaming the temporary table\r\n\r\n");
                sb.Append("DROP TABLE " + origTable.ObjectName.ToString() + ";\r\n");
                sb.Append("ALTER TABLE " + tmpname + " RENAME TO " + origTable.ObjectName.ToString() + ";\r\n");                

                // Re-create all indexes of the updated table
                bool found = false;
                foreach (SQLiteCreateIndexStatement cindex in dstSchema[SchemaObject.Index].Values)
                {
                    if (SQLiteParser.Utils.Chop(cindex.OnTable).ToLower() ==
                        SQLiteParser.Utils.Chop(updTable.ObjectName.ToString()).ToLower())
                    {
                        if (!found)
                        {
                            sb.Append("\r\n-- Creating associated indexes from scratch\r\n\r\n");
                            found = true;
                        }

                        sb.Append(cindex.ToString() + ";\r\n");
                    }
                } // foreach

                // Re-create all triggers of the updated table
                found = false;
                foreach (SQLiteCreateTriggerStatement trg in dstSchema[SchemaObject.Trigger].Values)
                {
                    if (trg.TableName.Equals(updTable.ObjectName))
                    {
                        if (!found)
                        {
                            sb.Append("\r\n-- Creating associated triggers from scratch\r\n\r\n");
                            found = true;
                        }

                        sb.Append(trg.ToString() + ";\r\n");
                    }
                } // foreach
            } // else
        }

        private static void ApplyIndexOrTriggerChanges(StringBuilder sb, SQLiteDdlStatement stmt,
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> srcSchema,
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> dstSchema)
        {
            // If there are any differences in indexes - apply them now
            List<SQLiteCreateIndexStatement> changedIndexes = null;
            List<SQLiteObjectName> removedIndexes = null;
            if (Utils.TableIndexesWereChanged(stmt.ObjectName.ToString(), srcSchema[SchemaObject.Index], dstSchema[SchemaObject.Index],
                out changedIndexes, out removedIndexes))
            {
                foreach (SQLiteObjectName iname in removedIndexes)
                    sb.Append("DROP INDEX IF EXISTS " + iname.ToString() + ";\r\n");

                foreach (SQLiteCreateIndexStatement idx in changedIndexes)
                {
                    sb.Append("DROP INDEX IF EXISTS " + idx.ObjectName.ToString() + ";\r\n");
                    sb.Append(idx.ToString() + ";\r\n");
                } // foreach
            }

            // If there are any differences in triggers - apply them now
            List<SQLiteCreateTriggerStatement> changedTriggers = null;
            List<SQLiteObjectName> removedTriggers = null;
            if (Utils.TableTriggersWereChanged(stmt.ObjectName.ToString(), srcSchema[SchemaObject.Trigger], dstSchema[SchemaObject.Trigger],
                out changedTriggers, out removedTriggers))
            {
                foreach (SQLiteObjectName iname in removedTriggers)
                    sb.Append("DROP TRIGGER IF EXISTS " + iname.ToString() + ";\r\n");

                foreach (SQLiteCreateTriggerStatement trg in changedTriggers)
                {
                    sb.Append("DROP TRIGGER IF EXISTS " + trg.ObjectName.ToString() + ";\r\n");
                    sb.Append(trg.ToString() + ";\r\n");
                } // foreach
            } // if
        }

        /// <summary>
        /// appends the SQL commands needed to delete a table from the target database
        /// </summary>
        /// <param name="sb">The string builder objcet</param>
        /// <param name="stmt">The table statement schema objcet</param>
        private static void DeleteTable(StringBuilder sb, SQLiteDdlStatement stmt)
        {
            string tableName = stmt.ObjectName.ToString();

            sb.Append("\r\nDROP TABLE " + tableName + ";\r\n");
        }

        /// <summary>
        /// Append the SQL commands needed to copy a table from a source database to
        /// the target database.
        /// </summary>
        /// <param name="sb">The string builder object</param>
        /// <param name="stmt">The CREATE TABLE schema object</param>
        /// <param name="srcSchema">The source schema from which the table is copied</param>
        private static void CopyTable(StringBuilder sb, SQLiteDdlStatement stmt, 
            Dictionary<SchemaObject, Dictionary<string, SQLiteDdlStatement>> srcSchema)
        {
            string tableName = stmt.ObjectName.ToString();

            // The table (and any associated triggers and/or indexes) does not exist in the
            // right database, so we need to create it from scratch.
            string create = stmt.ToString();
            sb.Append("\r\n"+create + ";\r\n");

            // Create any associated indexes
            Dictionary<string, SQLiteDdlStatement> indexes = srcSchema[SchemaObject.Index];
            foreach (SQLiteCreateIndexStatement cindex in indexes.Values)
            {
                if (SQLiteParser.Utils.Chop(cindex.OnTable).ToLower() ==
                    SQLiteParser.Utils.Chop(tableName).ToLower())
                {
                    sb.Append(cindex.ToString() + ";\r\n");
                }
            } // foreach

            // Now add CREATE for any triggers of this table
            Dictionary<string, SQLiteDdlStatement> triggers = srcSchema[SchemaObject.Trigger];
            foreach (SQLiteCreateTriggerStatement ctrig in triggers.Values)
            {
                if (SQLiteParser.Utils.Chop(ctrig.TableName.ToString()).ToLower() ==
                    SQLiteParser.Utils.Chop(tableName).ToLower())
                {
                    sb.Append(ctrig.ToString() + ";\r\n");
                }
            } // foreach
        }
        #endregion
    }

    public enum ChangeDirection
    {
        None = 0,

        LeftToRight = 1,

        RightToLeft = 2,
    }
}

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 GNU Lesser General Public License (LGPLv3)


Written By
Software Developer
Israel Israel
My name is Liron Levi and I'm developing software for fun & profit for 15 years already.

Comments and Discussions