diff --git a/DbUp.Support.SqlServer.Scripting/DbObjectScripter.cs b/DbUp.Support.SqlServer.Scripting/DbObjectScripter.cs index 20fa763..89791ca 100644 --- a/DbUp.Support.SqlServer.Scripting/DbObjectScripter.cs +++ b/DbUp.Support.SqlServer.Scripting/DbObjectScripter.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -18,7 +19,16 @@ namespace DbUp.Support.SqlServer.Scripting { public class DbObjectScripter { - private readonly string m_scrptingObjectRegEx = @"(CREATE|ALTER|DROP)\s*(TABLE|VIEW|PROCEDURE|PROC|FUNCTION|SYNONYM|TYPE) ([\w\[\]\-]+)?\.?([\w\[\]\-]*)"; + private const string SCRIPTING_OBJECT_REGEX = @"((CREATE|ALTER|DROP|CREATE\s*OR\s*ALTER)\s*(TABLE|VIEW|PROCEDURE|PROC|FUNCTION|SYNONYM|TYPE)\s*I?F?\s*E?X?I?S?T?S?\s*([\w\[\]\-]+)?\.?([\w\[\]\-]*))|(sp_rename{1,1}\s*'([\w\[\]\-]+)?\.?([\w\[\]\-]*)'\s*,\s*'([\w\[\]\-]*)')"; + private const int REGEX_INDEX_ACTION_TYPE = 2; + private const int REGEX_INDEX_OBJECT_TYPE = 3; + private const int REGEX_INDEX_SCHEMA_NAME = 4; + private const int REGEX_INDEX_OBJECT_NAME = 5; + private const int REGEX_INDEX_OBJECT_RENAME_SCHEMA = 7; + private const int REGEX_INDEX_OBJECT_RENAME_OLD_NAME = 8; + private const int REGEX_INDEX_OBJECT_RENAME_NEW_NAME = 9; + private readonly Regex m_targetDbObjectRegex = new Regex(SCRIPTING_OBJECT_REGEX, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); + private Options m_options; private string m_definitionDirectory; private SqlConnectionStringBuilder m_connectionBuilder; @@ -74,12 +84,19 @@ public ScripterResult ScriptAll() ScriptAllUserDefinedTypes(context); }); + var functionsScriptTask = Task.Run(() => + { + var context = GetDatabaseContext(true); + this.ScriptAllFunctions(context); + }); + Task.WaitAll( tablesScriptTask, viewsScriptTask, storedProceduresScriptTask, synonymsScriptTask, - udtScriptTask + udtScriptTask, + functionsScriptTask ); } catch (Exception ex) @@ -93,45 +110,139 @@ public ScripterResult ScriptAll() public ScripterResult ScriptMigrationTargets(IEnumerable migrationScripts) { - Regex targetDbObjectRegex = new Regex(m_scrptingObjectRegEx, - RegexOptions.IgnoreCase | RegexOptions.Multiline); + List scriptObjects = new List(migrationScripts.SelectMany(this.GetObjectsFromMigrationScripts)); + scriptObjects = CleanupScriptObjects(scriptObjects); + + return ScriptObjects(scriptObjects); + } - List scriptObjects = new List(); - foreach (SqlScript script in migrationScripts) + private IEnumerable GetObjectsFromMigrationScripts(SqlScript script) + { + //extract db object target(s) from scripts + MatchCollection matches = this.m_targetDbObjectRegex.Matches(script.Contents); + foreach (Match m in matches) { - //extract db object target(s) from scripts - MatchCollection matches = targetDbObjectRegex.Matches(script.Contents); - foreach (Match m in matches) + //if this group is empty, it means the second part of the regex matched (sp_rename) + if (!string.IsNullOrEmpty(m.Groups[REGEX_INDEX_ACTION_TYPE].Value)) { - string objectType = m.Groups[2].Value; - - ObjectTypeEnum type; - if (Enum.TryParse(objectType, true, out type)) + + if (Enum.TryParse(m.Groups[REGEX_INDEX_OBJECT_TYPE].Value, true, out var type)) { - ObjectActionEnum action = (ObjectActionEnum)Enum.Parse(typeof(ObjectActionEnum), m.Groups[1].Value, true); + //replace CREATE OR ALTER by CREATE + var actionString = m.Groups[REGEX_INDEX_ACTION_TYPE].Value.StartsWith(ObjectActionEnum.Create.ToString(), StringComparison.OrdinalIgnoreCase) + ? ObjectActionEnum.Create.ToString() + : m.Groups[REGEX_INDEX_ACTION_TYPE].Value; + + ObjectActionEnum action = (ObjectActionEnum)Enum.Parse(typeof(ObjectActionEnum), actionString, true); var scriptObject = new ScriptObject(type, action); - if (string.IsNullOrEmpty(m.Groups[4].Value) && !string.IsNullOrEmpty(m.Groups[3].Value)) + if (string.IsNullOrEmpty(m.Groups[REGEX_INDEX_OBJECT_NAME].Value) && !string.IsNullOrEmpty(m.Groups[REGEX_INDEX_SCHEMA_NAME].Value)) { - //no schema specified - scriptObject.ObjectName = m.Groups[3].Value; + //no schema specified. in that case, object name is in the schema group + scriptObject.ObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_SCHEMA_NAME].Value); } else { - scriptObject.ObjectSchema = m.Groups[3].Value; - scriptObject.ObjectName = m.Groups[4].Value; + scriptObject.ObjectSchema = RemoveBrackets(m.Groups[REGEX_INDEX_SCHEMA_NAME].Value); + scriptObject.ObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_NAME].Value); } - char[] removeCharacters = new char[] { '[', ']' }; - scriptObject.ObjectSchema = removeCharacters.Aggregate(scriptObject.ObjectSchema, (c1, c2) => c1.Replace(c2.ToString(), "")); - scriptObject.ObjectName = removeCharacters.Aggregate(scriptObject.ObjectName, (c1, c2) => c1.Replace(c2.ToString(), "")); - - scriptObjects.Add(scriptObject); + yield return scriptObject; + } + } + else + { + string schemaName; + string oldObjectName; + string newObjectName; + if (string.IsNullOrEmpty(m.Groups[REGEX_INDEX_OBJECT_RENAME_OLD_NAME].Value) && !string.IsNullOrEmpty(m.Groups[REGEX_INDEX_OBJECT_RENAME_SCHEMA].Value)) + { + //no schema specified. in that case, object name is in the schema group + schemaName = "dbo"; + oldObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_SCHEMA].Value); + newObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_OLD_NAME].Value); } + else + { + schemaName = m.Groups[REGEX_INDEX_OBJECT_RENAME_SCHEMA].Value; + oldObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_OLD_NAME].Value); + newObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_NEW_NAME].Value); + } + + var type = GetObjectTypeFromDb(schemaName, newObjectName); + + var scriptObjectDrop = new ScriptObject(type, ObjectActionEnum.Drop); + scriptObjectDrop.ObjectSchema = schemaName; + scriptObjectDrop.ObjectName = oldObjectName; + + yield return scriptObjectDrop; + + var scriptObjectCreate = new ScriptObject(type, ObjectActionEnum.Create); + scriptObjectCreate.ObjectSchema = schemaName; + scriptObjectCreate.ObjectName = newObjectName; + + yield return scriptObjectCreate; } } + } - return ScriptObjects(scriptObjects); + private static string RemoveBrackets(string @string) + { + char[] removeCharacters = { '[', ']' }; + return removeCharacters.Aggregate(@string, (c1, c2) => c1.Replace(c2.ToString(), "")); + } + + private ObjectTypeEnum GetObjectTypeFromDb(string schemaName, string objectName) + { + var context = this.GetDatabaseContext(false); + if (context.Database.Tables[objectName, schemaName] != null) + { + return ObjectTypeEnum.Table; + } + if (context.Database.Views[objectName, schemaName] != null) + { + return ObjectTypeEnum.View; + } + if (context.Database.Synonyms[objectName, schemaName] != null) + { + return ObjectTypeEnum.Synonym; + } + if (context.Database.StoredProcedures[objectName, schemaName] != null || context.Database.ExtendedStoredProcedures[objectName, schemaName] != null) + { + return ObjectTypeEnum.Procedure; + } + if (context.Database.UserDefinedFunctions[objectName, schemaName] != null) + { + return ObjectTypeEnum.Function; + } + if (context.Database.UserDefinedDataTypes[objectName, schemaName] != null || context.Database.UserDefinedTableTypes[objectName, schemaName] != null|| context.Database.UserDefinedTypes[objectName, schemaName] != null) + { + return ObjectTypeEnum.Type; + } + + return ObjectTypeEnum.Undefined; + } + + /// + /// Remove duplicates from a list of ScriptObjects to avoid double sripting of files and not run into errors with later droped objects + /// + /// + /// + private static List CleanupScriptObjects(List scriptObjects) + { + var preCleanUpScripts = new List(scriptObjects); + preCleanUpScripts.Reverse(); + + var cleanedUpScripts = new List(); + foreach (var script in preCleanUpScripts) + { + if (!cleanedUpScripts.Any(s => s.FullName.Equals(script.FullName, StringComparison.OrdinalIgnoreCase))) + { + cleanedUpScripts.Add(script); + } + } + + return cleanedUpScripts; } public ScripterResult ScriptObjects(IEnumerable objects) @@ -148,6 +259,8 @@ public ScripterResult ScriptObjects(IEnumerable objects) ScriptFunctions(context, objects.Where(o => o.ObjectType == ObjectTypeEnum.Function)); ScriptSynonyms(context, objects.Where(o => o.ObjectType == ObjectTypeEnum.Synonym)); ScriptUserDefinedTypes(context, objects.Where(o => o.ObjectType == ObjectTypeEnum.Type)); + + WarnUndefinedObjects(objects.Where(o => o.ObjectType == ObjectTypeEnum.Undefined)); } catch (Exception ex) { @@ -467,7 +580,15 @@ private void ScriptDefinition(ScriptObject dbObject, string outputDirectory, Fun } catch (Exception ex) { - m_log.WriteError(string.Format("Error when scripting definition for {0}: {1}", dbObject.ObjectName, ex.Message)); + m_log.WriteError(string.Format("Error when scripting definition for {0}.{1}: {2}", dbObject.ObjectSchema, dbObject.ObjectName, ex.Message)); + } + } + + private void WarnUndefinedObjects(IEnumerable dbObjects) + { + foreach (var dbObject in dbObjects) + { + m_log.WriteWarning(string.Format("The object {0}.{1} could not be scripted, since the object type was not identifyable. Normally this means, that the object has been dropped in the meantime. If necessary delete the file manually.", dbObject.ObjectSchema, dbObject.ObjectName)); } } @@ -482,6 +603,13 @@ private void SaveScript(ScriptObject scriptObject, StringCollection script, stri { sb.Append(str); sb.Append(Environment.NewLine); + + if (this.m_options.ScriptBatchTerminator) + { + sb.Append("GO"); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + } } m_log.WriteInformation(string.Format("Saving object definition: {0}", Path.Combine(outputDirectory, scriptObject.FileName))); diff --git a/DbUp.Support.SqlServer.Scripting/ObjectTypeEnum.cs b/DbUp.Support.SqlServer.Scripting/ObjectTypeEnum.cs index 57db66e..66d4ae0 100644 --- a/DbUp.Support.SqlServer.Scripting/ObjectTypeEnum.cs +++ b/DbUp.Support.SqlServer.Scripting/ObjectTypeEnum.cs @@ -9,6 +9,7 @@ namespace DbUp.Support.SqlServer.Scripting [Flags] public enum ObjectTypeEnum : int { + Undefined = 0, Table = 1, View = 2, Procedure = 4, diff --git a/DbUp.Support.SqlServer.Scripting/Options.cs b/DbUp.Support.SqlServer.Scripting/Options.cs index 3f69df2..d917801 100644 --- a/DbUp.Support.SqlServer.Scripting/Options.cs +++ b/DbUp.Support.SqlServer.Scripting/Options.cs @@ -46,6 +46,7 @@ public Options() public string FolderNameProcedures { get; set; } public string FolderNameFunctions { get; set; } public string FolderNameSynonyms { get; set; } + public bool ScriptBatchTerminator { get; set; } public ObjectTypeEnum ObjectsToInclude { get; set; } } } diff --git a/DbUp.Support.SqlServer.Scripting/ScriptingUpgrader.cs b/DbUp.Support.SqlServer.Scripting/ScriptingUpgrader.cs index e8330da..d300580 100644 --- a/DbUp.Support.SqlServer.Scripting/ScriptingUpgrader.cs +++ b/DbUp.Support.SqlServer.Scripting/ScriptingUpgrader.cs @@ -120,14 +120,24 @@ public DatabaseUpgradeResult Run(string[] args) } else { + var executedScriptsBeforeUpgrade = this.m_engine.GetExecutedScripts(); result = m_engine.PerformUpgrade(); - - if (result.Successful - && args.Any(a => "--fromconsole".Equals(a.Trim(), StringComparison.InvariantCultureIgnoreCase))) + if (args.Any(a => "--fromconsole".Equals(a.Trim(), StringComparison.InvariantCultureIgnoreCase))) { - this.Log.WriteInformation("Scripting changed database objects..."); - var scripter = new DbObjectScripter(this.ConnectionString, m_options, this.Log); - var scriptorResult = scripter.ScriptMigrationTargets(scriptsToExecute); + var scripter = new DbObjectScripter(this.ConnectionString, this.m_options, this.Log); + if (result.Successful) + { + this.Log.WriteInformation("Scripting changed database objects..."); + var scriptorResult = scripter.ScriptMigrationTargets(scriptsToExecute); + } + else + { + this.Log.WriteInformation("Scripting successfully changed database objects..."); + var executedScriptsAfterUpgrade = this.m_engine.GetExecutedScripts(); + var appliedScripts = scriptsToExecute.Where(s => executedScriptsAfterUpgrade.Except(executedScriptsBeforeUpgrade) + .Contains(s.Name)); + var scriptorResult = scripter.ScriptMigrationTargets(appliedScripts); + } } } } diff --git a/README.md b/README.md index ba5067e..54a586e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,22 @@ The following SQL Server object types are currently supported: * Stored Procedures * User Defined Functions * Synonyms +* User Defined Types + +## Statement Types +The following list shows which statement types are currently supported: + +* CREATE +* CREATE OR ALTER +* ALTER +* CREATE +* CREATE IF EXISTS +* Renaming with sp_rename + +## Known Issues +* Renaming with sp_rename +** Only the renaming of objects itself (like table, view, procedures, etc.) is supported, but not the renaming of columns, indexes, keys +** When dropping or again renaming an object after it has been renamed with sp_rename, those objects can not be properly scripted ## Script All Definitions You can run `Start-DatabaseScript` from the Package Manager Console to script all objects in the database. If working with an existing database, it is recommended to run this command initially so that all your definition files are saved.