Skip to content
180 changes: 154 additions & 26 deletions DbUp.Support.SqlServer.Scripting/DbObjectScripter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -93,45 +110,139 @@ public ScripterResult ScriptAll()

public ScripterResult ScriptMigrationTargets(IEnumerable<SqlScript> migrationScripts)
{
Regex targetDbObjectRegex = new Regex(m_scrptingObjectRegEx,
RegexOptions.IgnoreCase | RegexOptions.Multiline);
List<ScriptObject> scriptObjects = new List<ScriptObject>(migrationScripts.SelectMany(this.GetObjectsFromMigrationScripts));
scriptObjects = CleanupScriptObjects(scriptObjects);

return ScriptObjects(scriptObjects);
}

List<ScriptObject> scriptObjects = new List<ScriptObject>();
foreach (SqlScript script in migrationScripts)
private IEnumerable<ScriptObject> 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<ObjectTypeEnum>(objectType, true, out type))

if (Enum.TryParse<ObjectTypeEnum>(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;
}

/// <summary>
/// Remove duplicates from a list of ScriptObjects to avoid double sripting of files and not run into errors with later droped objects
/// </summary>
/// <param name="scriptObjects"></param>
/// <returns></returns>
private static List<ScriptObject> CleanupScriptObjects(List<ScriptObject> scriptObjects)
{
var preCleanUpScripts = new List<ScriptObject>(scriptObjects);
preCleanUpScripts.Reverse();

var cleanedUpScripts = new List<ScriptObject>();
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<ScriptObject> objects)
Expand All @@ -148,6 +259,8 @@ public ScripterResult ScriptObjects(IEnumerable<ScriptObject> 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)
{
Expand Down Expand Up @@ -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<ScriptObject> 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));
}
}

Expand All @@ -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)));
Expand Down
1 change: 1 addition & 0 deletions DbUp.Support.SqlServer.Scripting/ObjectTypeEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace DbUp.Support.SqlServer.Scripting
[Flags]
public enum ObjectTypeEnum : int
{
Undefined = 0,
Table = 1,
View = 2,
Procedure = 4,
Expand Down
1 change: 1 addition & 0 deletions DbUp.Support.SqlServer.Scripting/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
22 changes: 16 additions & 6 deletions DbUp.Support.SqlServer.Scripting/ScriptingUpgrader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down