Skip to content

Commit b61672e

Browse files
authored
Merge pull request #30 from crazycrank/master
Bugfix and several new Features
2 parents 26ce796 + f69bdd1 commit b61672e

File tree

5 files changed

+188
-32
lines changed

5 files changed

+188
-32
lines changed

DbUp.Support.SqlServer.Scripting/DbObjectScripter.cs

+154-26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.IO;
1111
using System.Linq;
1212
using System.Reflection;
13+
using System.Runtime.Remoting.Messaging;
1314
using System.Text;
1415
using System.Text.RegularExpressions;
1516
using System.Threading.Tasks;
@@ -18,7 +19,16 @@ namespace DbUp.Support.SqlServer.Scripting
1819
{
1920
public class DbObjectScripter
2021
{
21-
private readonly string m_scrptingObjectRegEx = @"(CREATE|ALTER|DROP)\s*(TABLE|VIEW|PROCEDURE|PROC|FUNCTION|SYNONYM|TYPE) ([\w\[\]\-]+)?\.?([\w\[\]\-]*)";
22+
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\[\]\-]*)')";
23+
private const int REGEX_INDEX_ACTION_TYPE = 2;
24+
private const int REGEX_INDEX_OBJECT_TYPE = 3;
25+
private const int REGEX_INDEX_SCHEMA_NAME = 4;
26+
private const int REGEX_INDEX_OBJECT_NAME = 5;
27+
private const int REGEX_INDEX_OBJECT_RENAME_SCHEMA = 7;
28+
private const int REGEX_INDEX_OBJECT_RENAME_OLD_NAME = 8;
29+
private const int REGEX_INDEX_OBJECT_RENAME_NEW_NAME = 9;
30+
private readonly Regex m_targetDbObjectRegex = new Regex(SCRIPTING_OBJECT_REGEX, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
31+
2232
private Options m_options;
2333
private string m_definitionDirectory;
2434
private SqlConnectionStringBuilder m_connectionBuilder;
@@ -74,12 +84,19 @@ public ScripterResult ScriptAll()
7484
ScriptAllUserDefinedTypes(context);
7585
});
7686

87+
var functionsScriptTask = Task.Run(() =>
88+
{
89+
var context = GetDatabaseContext(true);
90+
this.ScriptAllFunctions(context);
91+
});
92+
7793
Task.WaitAll(
7894
tablesScriptTask,
7995
viewsScriptTask,
8096
storedProceduresScriptTask,
8197
synonymsScriptTask,
82-
udtScriptTask
98+
udtScriptTask,
99+
functionsScriptTask
83100
);
84101
}
85102
catch (Exception ex)
@@ -93,45 +110,139 @@ public ScripterResult ScriptAll()
93110

94111
public ScripterResult ScriptMigrationTargets(IEnumerable<SqlScript> migrationScripts)
95112
{
96-
Regex targetDbObjectRegex = new Regex(m_scrptingObjectRegEx,
97-
RegexOptions.IgnoreCase | RegexOptions.Multiline);
113+
List<ScriptObject> scriptObjects = new List<ScriptObject>(migrationScripts.SelectMany(this.GetObjectsFromMigrationScripts));
114+
scriptObjects = CleanupScriptObjects(scriptObjects);
115+
116+
return ScriptObjects(scriptObjects);
117+
}
98118

99-
List<ScriptObject> scriptObjects = new List<ScriptObject>();
100-
foreach (SqlScript script in migrationScripts)
119+
private IEnumerable<ScriptObject> GetObjectsFromMigrationScripts(SqlScript script)
120+
{
121+
//extract db object target(s) from scripts
122+
MatchCollection matches = this.m_targetDbObjectRegex.Matches(script.Contents);
123+
foreach (Match m in matches)
101124
{
102-
//extract db object target(s) from scripts
103-
MatchCollection matches = targetDbObjectRegex.Matches(script.Contents);
104-
foreach (Match m in matches)
125+
//if this group is empty, it means the second part of the regex matched (sp_rename)
126+
if (!string.IsNullOrEmpty(m.Groups[REGEX_INDEX_ACTION_TYPE].Value))
105127
{
106-
string objectType = m.Groups[2].Value;
107-
108-
ObjectTypeEnum type;
109-
if (Enum.TryParse<ObjectTypeEnum>(objectType, true, out type))
128+
129+
if (Enum.TryParse<ObjectTypeEnum>(m.Groups[REGEX_INDEX_OBJECT_TYPE].Value, true, out var type))
110130
{
111-
ObjectActionEnum action = (ObjectActionEnum)Enum.Parse(typeof(ObjectActionEnum), m.Groups[1].Value, true);
131+
//replace CREATE OR ALTER by CREATE
132+
var actionString = m.Groups[REGEX_INDEX_ACTION_TYPE].Value.StartsWith(ObjectActionEnum.Create.ToString(), StringComparison.OrdinalIgnoreCase)
133+
? ObjectActionEnum.Create.ToString()
134+
: m.Groups[REGEX_INDEX_ACTION_TYPE].Value;
135+
136+
ObjectActionEnum action = (ObjectActionEnum)Enum.Parse(typeof(ObjectActionEnum), actionString, true);
112137
var scriptObject = new ScriptObject(type, action);
113138

114-
if (string.IsNullOrEmpty(m.Groups[4].Value) && !string.IsNullOrEmpty(m.Groups[3].Value))
139+
if (string.IsNullOrEmpty(m.Groups[REGEX_INDEX_OBJECT_NAME].Value) && !string.IsNullOrEmpty(m.Groups[REGEX_INDEX_SCHEMA_NAME].Value))
115140
{
116-
//no schema specified
117-
scriptObject.ObjectName = m.Groups[3].Value;
141+
//no schema specified. in that case, object name is in the schema group
142+
scriptObject.ObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_SCHEMA_NAME].Value);
118143
}
119144
else
120145
{
121-
scriptObject.ObjectSchema = m.Groups[3].Value;
122-
scriptObject.ObjectName = m.Groups[4].Value;
146+
scriptObject.ObjectSchema = RemoveBrackets(m.Groups[REGEX_INDEX_SCHEMA_NAME].Value);
147+
scriptObject.ObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_NAME].Value);
123148
}
124149

125-
char[] removeCharacters = new char[] { '[', ']' };
126-
scriptObject.ObjectSchema = removeCharacters.Aggregate(scriptObject.ObjectSchema, (c1, c2) => c1.Replace(c2.ToString(), ""));
127-
scriptObject.ObjectName = removeCharacters.Aggregate(scriptObject.ObjectName, (c1, c2) => c1.Replace(c2.ToString(), ""));
128-
129-
scriptObjects.Add(scriptObject);
150+
yield return scriptObject;
151+
}
152+
}
153+
else
154+
{
155+
string schemaName;
156+
string oldObjectName;
157+
string newObjectName;
158+
if (string.IsNullOrEmpty(m.Groups[REGEX_INDEX_OBJECT_RENAME_OLD_NAME].Value) && !string.IsNullOrEmpty(m.Groups[REGEX_INDEX_OBJECT_RENAME_SCHEMA].Value))
159+
{
160+
//no schema specified. in that case, object name is in the schema group
161+
schemaName = "dbo";
162+
oldObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_SCHEMA].Value);
163+
newObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_OLD_NAME].Value);
130164
}
165+
else
166+
{
167+
schemaName = m.Groups[REGEX_INDEX_OBJECT_RENAME_SCHEMA].Value;
168+
oldObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_OLD_NAME].Value);
169+
newObjectName = RemoveBrackets(m.Groups[REGEX_INDEX_OBJECT_RENAME_NEW_NAME].Value);
170+
}
171+
172+
var type = GetObjectTypeFromDb(schemaName, newObjectName);
173+
174+
var scriptObjectDrop = new ScriptObject(type, ObjectActionEnum.Drop);
175+
scriptObjectDrop.ObjectSchema = schemaName;
176+
scriptObjectDrop.ObjectName = oldObjectName;
177+
178+
yield return scriptObjectDrop;
179+
180+
var scriptObjectCreate = new ScriptObject(type, ObjectActionEnum.Create);
181+
scriptObjectCreate.ObjectSchema = schemaName;
182+
scriptObjectCreate.ObjectName = newObjectName;
183+
184+
yield return scriptObjectCreate;
131185
}
132186
}
187+
}
133188

134-
return ScriptObjects(scriptObjects);
189+
private static string RemoveBrackets(string @string)
190+
{
191+
char[] removeCharacters = { '[', ']' };
192+
return removeCharacters.Aggregate(@string, (c1, c2) => c1.Replace(c2.ToString(), ""));
193+
}
194+
195+
private ObjectTypeEnum GetObjectTypeFromDb(string schemaName, string objectName)
196+
{
197+
var context = this.GetDatabaseContext(false);
198+
if (context.Database.Tables[objectName, schemaName] != null)
199+
{
200+
return ObjectTypeEnum.Table;
201+
}
202+
if (context.Database.Views[objectName, schemaName] != null)
203+
{
204+
return ObjectTypeEnum.View;
205+
}
206+
if (context.Database.Synonyms[objectName, schemaName] != null)
207+
{
208+
return ObjectTypeEnum.Synonym;
209+
}
210+
if (context.Database.StoredProcedures[objectName, schemaName] != null || context.Database.ExtendedStoredProcedures[objectName, schemaName] != null)
211+
{
212+
return ObjectTypeEnum.Procedure;
213+
}
214+
if (context.Database.UserDefinedFunctions[objectName, schemaName] != null)
215+
{
216+
return ObjectTypeEnum.Function;
217+
}
218+
if (context.Database.UserDefinedDataTypes[objectName, schemaName] != null || context.Database.UserDefinedTableTypes[objectName, schemaName] != null|| context.Database.UserDefinedTypes[objectName, schemaName] != null)
219+
{
220+
return ObjectTypeEnum.Type;
221+
}
222+
223+
return ObjectTypeEnum.Undefined;
224+
}
225+
226+
/// <summary>
227+
/// Remove duplicates from a list of ScriptObjects to avoid double sripting of files and not run into errors with later droped objects
228+
/// </summary>
229+
/// <param name="scriptObjects"></param>
230+
/// <returns></returns>
231+
private static List<ScriptObject> CleanupScriptObjects(List<ScriptObject> scriptObjects)
232+
{
233+
var preCleanUpScripts = new List<ScriptObject>(scriptObjects);
234+
preCleanUpScripts.Reverse();
235+
236+
var cleanedUpScripts = new List<ScriptObject>();
237+
foreach (var script in preCleanUpScripts)
238+
{
239+
if (!cleanedUpScripts.Any(s => s.FullName.Equals(script.FullName, StringComparison.OrdinalIgnoreCase)))
240+
{
241+
cleanedUpScripts.Add(script);
242+
}
243+
}
244+
245+
return cleanedUpScripts;
135246
}
136247

137248
public ScripterResult ScriptObjects(IEnumerable<ScriptObject> objects)
@@ -148,6 +259,8 @@ public ScripterResult ScriptObjects(IEnumerable<ScriptObject> objects)
148259
ScriptFunctions(context, objects.Where(o => o.ObjectType == ObjectTypeEnum.Function));
149260
ScriptSynonyms(context, objects.Where(o => o.ObjectType == ObjectTypeEnum.Synonym));
150261
ScriptUserDefinedTypes(context, objects.Where(o => o.ObjectType == ObjectTypeEnum.Type));
262+
263+
WarnUndefinedObjects(objects.Where(o => o.ObjectType == ObjectTypeEnum.Undefined));
151264
}
152265
catch (Exception ex)
153266
{
@@ -467,7 +580,15 @@ private void ScriptDefinition(ScriptObject dbObject, string outputDirectory, Fun
467580
}
468581
catch (Exception ex)
469582
{
470-
m_log.WriteError(string.Format("Error when scripting definition for {0}: {1}", dbObject.ObjectName, ex.Message));
583+
m_log.WriteError(string.Format("Error when scripting definition for {0}.{1}: {2}", dbObject.ObjectSchema, dbObject.ObjectName, ex.Message));
584+
}
585+
}
586+
587+
private void WarnUndefinedObjects(IEnumerable<ScriptObject> dbObjects)
588+
{
589+
foreach (var dbObject in dbObjects)
590+
{
591+
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));
471592
}
472593
}
473594

@@ -482,6 +603,13 @@ private void SaveScript(ScriptObject scriptObject, StringCollection script, stri
482603
{
483604
sb.Append(str);
484605
sb.Append(Environment.NewLine);
606+
607+
if (this.m_options.ScriptBatchTerminator)
608+
{
609+
sb.Append("GO");
610+
sb.Append(Environment.NewLine);
611+
sb.Append(Environment.NewLine);
612+
}
485613
}
486614

487615
m_log.WriteInformation(string.Format("Saving object definition: {0}", Path.Combine(outputDirectory, scriptObject.FileName)));

DbUp.Support.SqlServer.Scripting/ObjectTypeEnum.cs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace DbUp.Support.SqlServer.Scripting
99
[Flags]
1010
public enum ObjectTypeEnum : int
1111
{
12+
Undefined = 0,
1213
Table = 1,
1314
View = 2,
1415
Procedure = 4,

DbUp.Support.SqlServer.Scripting/Options.cs

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public Options()
4646
public string FolderNameProcedures { get; set; }
4747
public string FolderNameFunctions { get; set; }
4848
public string FolderNameSynonyms { get; set; }
49+
public bool ScriptBatchTerminator { get; set; }
4950
public ObjectTypeEnum ObjectsToInclude { get; set; }
5051
}
5152
}

DbUp.Support.SqlServer.Scripting/ScriptingUpgrader.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,24 @@ public DatabaseUpgradeResult Run(string[] args)
120120
}
121121
else
122122
{
123+
var executedScriptsBeforeUpgrade = this.m_engine.GetExecutedScripts();
123124
result = m_engine.PerformUpgrade();
124-
125-
if (result.Successful
126-
&& args.Any(a => "--fromconsole".Equals(a.Trim(), StringComparison.InvariantCultureIgnoreCase)))
125+
if (args.Any(a => "--fromconsole".Equals(a.Trim(), StringComparison.InvariantCultureIgnoreCase)))
127126
{
128-
this.Log.WriteInformation("Scripting changed database objects...");
129-
var scripter = new DbObjectScripter(this.ConnectionString, m_options, this.Log);
130-
var scriptorResult = scripter.ScriptMigrationTargets(scriptsToExecute);
127+
var scripter = new DbObjectScripter(this.ConnectionString, this.m_options, this.Log);
128+
if (result.Successful)
129+
{
130+
this.Log.WriteInformation("Scripting changed database objects...");
131+
var scriptorResult = scripter.ScriptMigrationTargets(scriptsToExecute);
132+
}
133+
else
134+
{
135+
this.Log.WriteInformation("Scripting successfully changed database objects...");
136+
var executedScriptsAfterUpgrade = this.m_engine.GetExecutedScripts();
137+
var appliedScripts = scriptsToExecute.Where(s => executedScriptsAfterUpgrade.Except(executedScriptsBeforeUpgrade)
138+
.Contains(s.Name));
139+
var scriptorResult = scripter.ScriptMigrationTargets(appliedScripts);
140+
}
131141
}
132142
}
133143
}

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ The following SQL Server object types are currently supported:
6969
* Stored Procedures
7070
* User Defined Functions
7171
* Synonyms
72+
* User Defined Types
73+
74+
## Statement Types
75+
The following list shows which statement types are currently supported:
76+
77+
* CREATE
78+
* CREATE OR ALTER
79+
* ALTER
80+
* CREATE
81+
* CREATE IF EXISTS
82+
* Renaming with sp_rename
83+
84+
## Known Issues
85+
* Renaming with sp_rename
86+
** Only the renaming of objects itself (like table, view, procedures, etc.) is supported, but not the renaming of columns, indexes, keys
87+
** When dropping or again renaming an object after it has been renamed with sp_rename, those objects can not be properly scripted
7288

7389
## Script All Definitions
7490
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.

0 commit comments

Comments
 (0)