diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..37b070f --- /dev/null +++ b/.hgignore @@ -0,0 +1,10 @@ +glob:bin +glob:obj +glob:*.user +glob:*.suo +syntax: glob +*.docstates +*.nupkg +*.orig +*.rej +StyleCop.Cache diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000..86a1b29 --- /dev/null +++ b/.hgtags @@ -0,0 +1,53 @@ +5e4ddcbfbd2b78a560b84f5b30050f2a9121dd88 Version 1.0.1 +4031ecdd1c02d5bc9e5ae359992dd69a4fb04769 Version 1.0.2 +ecfad38b4cb6b4f720546fdafc2b4d858af43195 Version 1.0.3 +b111e60e44f736a5f7cfab0c2c6001cdee96da90 Version 1.0.4 +249b78592d69d3d4bf070857807ff8c1b3e2cf62 Version 1.0.5 +b4d4eb5d3fe91f85df9c8d8b008bff40afd0aeec Version 1.0.6 +c83e1e89bdf810e269436a0290e38b6a0594f279 Version 1.0.7 +7a9cd869b1b2ddd672bc33a9b5a76ae2e3c8bfd8 Version 1.0.8 +e41312b456e7ec8b5bc6fd8c6d77a35e8783766f Version 1.0.9 +d13f0a41b69846edca2b1f37b729049df9f2264b Version 1.0.10 +b5ed27bcd0705acdf57af68a7073279b4dc5b3c1 Version 1.0.11 +0526eb76242696298e9ee4140ff9eea930856772 Version 1.0.12 +c8e7af9dd7e29e12adcd0f0a32d744dc2a6e5f38 Version 1.0.13 +a55d3a5abbead8b6a222daad1921252da8222bec Version 1.0.14 +821c59d83416b824038f8f0312e28f91afeeb74a Version 1.0.15 +cfc4ae0885260dfe00be704de22bb2b1c7f1fb3f Version 1.0.16 +dfea6a3b42be9fb52d7de7d0bda731d2f4137a73 Version 1.0.17 +44b192ae01c12cc9c309504c72a0cab8f1227faf Version 1.0.18 +ae730615c5a53cbb98908f598c25ee4073458d10 Version 1.0.19 +973f89697b7653236ba78dbbde8be0ce76445902 Version 1.0.20 +5698a3a9f7609806b1925a18da81824aa3986b04 Version 1.0.21 +a65e3d0066462ae0fc858a53195c5a65b14edc27 Version 1.0.22 +8d98491b2f793730669d8971771daa3c43c5e582 Version 1.0.23 +8b684124731be34a8499e575fe23d522985ebce4 Version 1.0.24 +545bdf79747805133639757cba896b0e33142833 Version 1.0.25 +79e0d34d8d0e70843edb8396e9e130fb6db6a9c9 Version 1.0.26 +dee91b2c2c5866a12d3c926984430903acaf9080 Version 1.0.27 +303a7b6ad3eebfd5b486b6cbbf7b3e6dfd94ec85 Version 1.0.28 +c5d5721657b812666dd875aaa9d2ae56f90a43ff Version 1.0.29 +3dbf693e4d056fbe7ba8d6c129ed4d357ba65878 Version 1.0.30 +894fca4c726217d2ed7f977b1acd9b78a8f55fa7 Version 1.0.31 +192d936e8baf8d0a9d5003bc728236e59a4f9342 Version 1.0.32 +a843eee6ff188897dc986e53326ea043488f472d Version 1.0.33 +d6d50fd5174010533232bcdbeece355cd50626d7 Version 1.0.35 +1913031ca15d7138611af06a506abe12bee09214 Version 1.0.36 +70182d27f26da78e07180864a6bffc864c8dd5ef Version 1.0.37 +eec9b7a2237049340aeff9cc7bc289a18e2c3e06 Version 1.0.38 +7675ce28cb3bc9f1e2582d1b3662de49c0fc2d48 Version 1.0.39 +213b9ec5fd91c2383140c4b64ab9153136c839ad Version 1.0.40 +3851adb2c0f52ccf40e40e974d797fe5a69b8bb5 Version 1.0.41 +bae702c80195555e8284a150d0c7d66606331799 Version 1.0.42 +8b41b58cda9172fc4219ec0ef6d8261e1c1208a6 Version 1.0.43 +70a69b1fce4b8b91aa490018fbb180b61ff0e52b Version 1.0.44 +ab6f5fc8a811b0dbae74936264cea19e59b7847f Version 1.0.45 +2d0297caf946905400cc1e124b86a35d7eb49efa Version 1.0.46 +26620351f1e89ace8378da67da946495c0249f34 Version 1.0.47 +8c1e7b850f46da3cb81d784d67ba0b6c5de2bbbc Version 1.0.48 +d5ef0f4380c409776397699f8b0b351b91611bad Version 1.0.49 +dd129be56543fd83a795a461d9ee1ef78a350423 Version 1.0.50 +bc5490b578b131a1aa066cd1afa766031f45d044 Version 1.0.51 +812358c282b49b125feab7b0ce28c789d93067d4 Version 1.0.52 +cb26259f8e436dbc73962dd53ec232d57eae1383 Version 1.0.53 +8fde7d2b4756511c8d394657362bd0c0349ef2e8 Version 1.0.54 diff --git a/.nuget/packages.config b/.nuget/packages.config new file mode 100644 index 0000000..1b81f11 --- /dev/null +++ b/.nuget/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Building.txt b/Building.txt new file mode 100644 index 0000000..dd5df9b --- /dev/null +++ b/Building.txt @@ -0,0 +1,5 @@ +Notes for Building Insight.Database.Schema +========================================== + +* Release builds require FXCop 10.0 be installed on your system. +* Packaging the assembly requires that NuGet.exe be in your path. \ No newline at end of file diff --git a/Documents/Insight Schema Generator.doc b/Documents/Insight Schema Generator.doc new file mode 100644 index 0000000..bdd5cec Binary files /dev/null and b/Documents/Insight Schema Generator.doc differ diff --git a/Documents/Insight Schema Installer.doc b/Documents/Insight Schema Installer.doc new file mode 100644 index 0000000..4c893c8 Binary files /dev/null and b/Documents/Insight Schema Installer.doc differ diff --git a/Insight.Database.Schema/ConvertTableEventArgs.cs b/Insight.Database.Schema/ConvertTableEventArgs.cs new file mode 100644 index 0000000..ccc6049 --- /dev/null +++ b/Insight.Database.Schema/ConvertTableEventArgs.cs @@ -0,0 +1,57 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Text; + +#endregion + +namespace Insight.Database.Schema +{ + /// + /// Arguments for the OnConvertTable event + /// + public class ConvertTableEventArgs : SchemaEventArgs + { + /// + /// The name of the table containing the pre-converted data + /// + /// The name of the table containing the pre-converted data + public string BeforeTableName { get { return _beforeTableName; } } + private string _beforeTableName; + + /// + /// The name of the table to convert the data into + /// + /// The name of the table containing the pre-converted data + public string AfterTableName { get { return _afterTableName; } } + private string _afterTableName; + + /// + /// The SchemaInstaller connection to the database + /// + /// The name of the table containing the pre-converted data + public SqlConnection Connection { get { return _connection; } } + private SqlConnection _connection; + + /// + /// Tells the SchemaInstaller if the table data has been converted + /// + /// The name of the table containing the pre-converted data + public bool Converted + { + get { return _converted; } + set { _converted = value; } + } + private bool _converted; + + internal ConvertTableEventArgs (SchemaEventType eventType, SchemaObject table, SqlConnection connection, string beforeTable, string afterTable) : + base (eventType, table) + { + _connection = connection; + _beforeTableName = beforeTable; + _afterTableName = afterTable; + } + } +} diff --git a/Insight.Database.Schema/FxCop.FxCop b/Insight.Database.Schema/FxCop.FxCop new file mode 100644 index 0000000..9f71f68 --- /dev/null +++ b/Insight.Database.Schema/FxCop.FxCop @@ -0,0 +1,122 @@ + + + + True + $(FxCopDir)\Xml\FxCopReport.xsl + + + + + + True + True + True + 10 + 1 + + False + + False + 120 + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'Insight.Database.Schema.dll' + + + + + + + + + + + + + + + + + + Login + 'SchemaObjectType.Login' + LogOn + + + + + + + + + + + + + + PreScript + 'SchemaObjectType.UserPreScript' + Prescript + + + + + + + + + + + + + + + + + + + + + Code review completed + + + + + Sign {0} with a strong name key. + + + The compound word '{0}' in member name {1} exists as a discrete term. If your usage is intended to be single word, case it as '{2}' or strip the first token entirely if it represents any sort of Hungarian notation. + + + Replace the term '{0}' in member name {1} with the preferred alternate '{2}'. + + + + \ No newline at end of file diff --git a/Insight.Database.Schema/Insight.Database.Schema.csproj b/Insight.Database.Schema/Insight.Database.Schema.csproj new file mode 100644 index 0000000..a6a115a --- /dev/null +++ b/Insight.Database.Schema/Insight.Database.Schema.csproj @@ -0,0 +1,86 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {FBD1C65F-9054-42C7-BA0E-48A922C4C747} + Library + Properties + Insight.Database.Schema + Insight.Database.Schema + v4.0 + 512 + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Insight.Database.Schema/Insight.Database.Schema.nuspec b/Insight.Database.Schema/Insight.Database.Schema.nuspec new file mode 100644 index 0000000..3aa1405 --- /dev/null +++ b/Insight.Database.Schema/Insight.Database.Schema.nuspec @@ -0,0 +1,14 @@ + + + + Insight.Database.Schema + 1.0.0.0 + Insight.Database.Schema + Jon Wagner + SQL database schema setup engine + en-US + + + + + \ No newline at end of file diff --git a/Insight.Database.Schema/Properties/AssemblyInfo.cs b/Insight.Database.Schema/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..abe3b97 --- /dev/null +++ b/Insight.Database.Schema/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.235 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; +using System.Resources; + +[assembly: AssemblyTitle("Insight.Database Schema")] +[assembly: AssemblyCompany("Insight")] +[assembly: AssemblyProduct("Insight")] +[assembly: AssemblyCopyright("Copyright © Jon Wagner, used by permission")] +[assembly: ComVisible(false)] +[assembly: CLSCompliant(true)] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: NeutralResourcesLanguageAttribute("en-US")] diff --git a/Insight.Database.Schema/Properties/Resources.Designer.cs b/Insight.Database.Schema/Properties/Resources.Designer.cs new file mode 100644 index 0000000..398a5db --- /dev/null +++ b/Insight.Database.Schema/Properties/Resources.Designer.cs @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:2.0.50727.1318 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Insight.Database.Schema.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Insight.Database.Schema.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Cannot determine the type of the SQL script. SQL: {0}. + /// + internal static string CannotDetermineScriptType { + get { + return ResourceManager.GetString("CannotDetermineScriptType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot determine table name from index name '{0}'.. + /// + internal static string CannotGetTableNameFromIndexName { + get { + return ResourceManager.GetString("CannotGetTableNameFromIndexName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot update a table that contains only primary keys.. + /// + internal static string CannotUpdatePKOnlyTable { + get { + return ResourceManager.GetString("CannotUpdatePKOnlyTable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Created. + /// + internal static string Created { + get { + return ResourceManager.GetString("Created", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creating. + /// + internal static string Creating { + get { + return ResourceManager.GetString("Creating", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dropping. + /// + internal static string Dropping { + get { + return ResourceManager.GetString("Dropping", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot add two objects with the same name '{0}' to the schema.. + /// + internal static string DuplicateObjectName { + get { + return ResourceManager.GetString("DuplicateObjectName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class {0} requires a DatabaseTableAttribute to use DbIndexAttribute.. + /// + internal static string IndexWithoutTable { + get { + return ResourceManager.GetString("IndexWithoutTable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name '{0}' is not a valid name for a SQL object.. + /// + internal static string InvalidSqlObjectName { + get { + return ResourceManager.GetString("InvalidSqlObjectName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated. + /// + internal static string Updated { + get { + return ResourceManager.GetString("Updated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updating. + /// + internal static string Updating { + get { + return ResourceManager.GetString("Updating", resourceCulture); + } + } + } +} diff --git a/Insight.Database.Schema/Properties/Resources.resx b/Insight.Database.Schema/Properties/Resources.resx new file mode 100644 index 0000000..d32ee2f --- /dev/null +++ b/Insight.Database.Schema/Properties/Resources.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot determine the type of the SQL script. SQL: {0} + + + Cannot determine table name from index name '{0}'. + + + Cannot update a table that contains only primary keys. + + + Created + + + Creating + + + Dropping + + + Cannot add two objects with the same name '{0}' to the schema. + + + Class {0} requires a DatabaseTableAttribute to use DbIndexAttribute. + + + The name '{0}' is not a valid name for a SQL object. + + + Updated + + + Updating + + \ No newline at end of file diff --git a/Insight.Database.Schema/Properties/Settings.Designer.cs b/Insight.Database.Schema/Properties/Settings.Designer.cs new file mode 100644 index 0000000..58ed4f0 --- /dev/null +++ b/Insight.Database.Schema/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:2.0.50727.1318 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Insight.Database.Schema.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "9.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Insight.Database.Schema/Properties/Settings.settings b/Insight.Database.Schema/Properties/Settings.settings new file mode 100644 index 0000000..3dcc2b6 --- /dev/null +++ b/Insight.Database.Schema/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Insight.Database.Schema/SchemaEventArgs.cs b/Insight.Database.Schema/SchemaEventArgs.cs new file mode 100644 index 0000000..080e1bf --- /dev/null +++ b/Insight.Database.Schema/SchemaEventArgs.cs @@ -0,0 +1,51 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Text; + +#endregion + +namespace Insight.Database.Schema +{ + /// + /// Arguments for a Schema change event + /// + public class SchemaEventArgs : EventArgs + { + /// + /// The type of Schema event + /// + /// The type of Schema event + public SchemaEventType EventType { get { return _eventType; } } + private SchemaEventType _eventType; + + /// + /// The name of the schema object + /// + /// The type of Schema event + public string ObjectName { get { return _objectName; } } + private string _objectName; + + /// + /// The schema object being updated. + /// + /// The type of Schema event + /// This is null for a drop object event + public SchemaObject SchemaObject { get { return _schemaObject; } } + private SchemaObject _schemaObject; + + internal SchemaEventArgs (SchemaEventType eventType, string objectName) + { + _eventType = eventType; + _objectName = objectName; + } + + internal SchemaEventArgs (SchemaEventType eventType, SchemaObject schemaObject) + { + _eventType = eventType; + _schemaObject = schemaObject; + _objectName = schemaObject.Name; + } + } +} diff --git a/Insight.Database.Schema/SchemaEventConsoleLogger.cs b/Insight.Database.Schema/SchemaEventConsoleLogger.cs new file mode 100644 index 0000000..9d91d42 --- /dev/null +++ b/Insight.Database.Schema/SchemaEventConsoleLogger.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Insight.Database.Schema +{ + /// + /// Logs console events to the console + /// + public class SchemaEventConsoleLogger : SchemaEventLogger + { + public override void LogSchemaChange (SchemaEventArgs se) + { + Console.WriteLine (FormatEventArgs (se)); + } + } +} diff --git a/Insight.Database.Schema/SchemaEventLogger.cs b/Insight.Database.Schema/SchemaEventLogger.cs new file mode 100644 index 0000000..7beab4e --- /dev/null +++ b/Insight.Database.Schema/SchemaEventLogger.cs @@ -0,0 +1,117 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +using Insight.Database.Schema.Properties; +#endregion + +namespace Insight.Database.Schema +{ + #region SchemaEventLogger Class + /// + /// Logs events from a schema installer. + /// + public abstract class SchemaEventLogger + { + #region Constructors + /// + /// Construct an event logger + /// + protected SchemaEventLogger () + { + _onSchemaChange = new EventHandler (OnSchemaChange); + } + + /// + /// Construct an event logger and attach it to a schema installer. + /// + /// The installer to monitor + protected SchemaEventLogger (SchemaInstaller installer) : this() + { + Attach (installer); + } + #endregion + + #region Attach/Detach Methods + /// + /// Attach to the events of a SchemaInstaller + /// + /// The installer to attach to + public void Attach (SchemaInstaller installer) + { + installer.DroppingObject += _onSchemaChange; + installer.UpdatingTable += _onSchemaChange; + installer.UpdatedTable += _onSchemaChange; + installer.CreatingObject += _onSchemaChange; + installer.CreatedObject += _onSchemaChange; + } + + /// + /// Detach from the events of a SchemaInstaller + /// + /// The installer to detach from + public void Detach (SchemaInstaller installer) + { + installer.DroppingObject -= _onSchemaChange; + installer.UpdatingTable -= _onSchemaChange; + installer.UpdatedTable -= _onSchemaChange; + installer.CreatingObject -= _onSchemaChange; + installer.CreatedObject -= _onSchemaChange; + } + + private EventHandler _onSchemaChange; + #endregion + + #region Event Handlers + /// + /// Called when a schema event is received + /// + /// The schema event + public abstract void LogSchemaChange (SchemaEventArgs se); + + private void OnSchemaChange (object sender, SchemaEventArgs se) + { + LogSchemaChange (se); + } + #endregion + + #region Support Methods + /// + /// Format a schema event into a string + /// + /// The schema event to format + /// The formatted event + protected static string FormatEventArgs (SchemaEventArgs se) + { + string eventName; + switch (se.EventType) + { + case SchemaEventType.BeforeDrop: + eventName = Resources.Dropping; + break; + case SchemaEventType.BeforeTableUpdate: + eventName = Resources.Updating; + break; + case SchemaEventType.AfterTableUpdate: + eventName = Resources.Updated; + break; + case SchemaEventType.BeforeCreate: + eventName = Resources.Creating; + break; + case SchemaEventType.AfterCreate: + eventName = Resources.Created; + break; + default: + eventName = Enum.Format (typeof (SchemaEventType), se.EventType, "G"); + break; + } + + return String.Format (CultureInfo.CurrentCulture, "{0} {1}", eventName, se.ObjectName); + } + #endregion + } + #endregion +} diff --git a/Insight.Database.Schema/SchemaEventType.cs b/Insight.Database.Schema/SchemaEventType.cs new file mode 100644 index 0000000..97a3526 --- /dev/null +++ b/Insight.Database.Schema/SchemaEventType.cs @@ -0,0 +1,24 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Text; + +#endregion + +namespace Insight.Database.Schema +{ + /// + /// The type of schema event + /// + public enum SchemaEventType + { + BeforeDrop, + BeforeTableUpdate, + AfterTableUpdate, + BeforeCreate, + AfterCreate, + ConvertTable, + MissingObject + } +} diff --git a/Insight.Database.Schema/SchemaInstaller.cs b/Insight.Database.Schema/SchemaInstaller.cs new file mode 100644 index 0000000..db25c20 --- /dev/null +++ b/Insight.Database.Schema/SchemaInstaller.cs @@ -0,0 +1,1324 @@ +#region Using directives +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Data; +using System.Data.SqlClient; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Transactions; + +using Insight.Database.Schema.Properties; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Common; +#endregion + +namespace Insight.Database.Schema +{ + #region SchemaInstaller Class + /// + /// Installs, upgrades, and uninstalls objects from a database + /// + public sealed class SchemaInstaller : IDisposable + { + #region Constructors + /// + /// Create a SchemaInstaller that is connected to a given database + /// + /// A connection string to the server. If the database does not exist, then a connection to the master database should be used. + /// The name of the database to edit + /// If connectionString is null + /// If the database connection cannot be established + /// If the database name contains invalid characters + /// The database connection is held open for the lifetime of the SchemaInstaller. + public SchemaInstaller (string connectionString, string databaseName) + { + // we need a connection string + if (connectionString == null) throw new ArgumentNullException ("connectionString"); + if (databaseName == null) throw new ArgumentNullException ("databaseName"); + AssertValidSqlName (databaseName); + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder (connectionString); + builder.Pooling = false; + // always use master + builder.InitialCatalog = "master"; + _connectionString = builder.ConnectionString; + _databaseName = databaseName; + } + #endregion + + #region Database Utility Methods + /// + /// Create a database on the specified connection if it does not exist + /// + /// True if the database was created, false if it already exists + /// If the database name is invalid + public bool CreateDatabase () + { + using (_connection = new SqlConnection (_connectionString)) + { + _connection.Open (); + + // see if the database already exists + bool createDatabase = !DatabaseExists (); + + // only create the database if it doesn't exist + if (createDatabase) + ExecuteNonQuery (String.Format (CultureInfo.InvariantCulture, "CREATE DATABASE {0}", _databaseName)); + + return createDatabase; + } + } + + /// + /// Drop a database if it exists + /// + /// True if the database was dropped, false if it did not exist + /// If the database name is invalid or cannot be dropped + public bool DropDatabase () + { + using (_connection = new SqlConnection (_connectionString)) + { + _connection.Open (); + + // if database does not exist, then don't delete it + if (!DatabaseExists()) + return false; + + // switch to the master database to get away from an open connection + _connection.ChangeDatabase ("master"); + + // set the database to single user mode, effectively dropping all connections except the current + // connection. + ExecuteNonQuery(String.Format(CultureInfo.InvariantCulture, "exec sp_dboption N'{0}', N'single', N'true'", _databaseName)); + + // drop the database + ExecuteNonQuery (String.Format (CultureInfo.InvariantCulture, "DROP DATABASE {0}", _databaseName)); + return true; + } + } + #endregion + + #region Schema Installation Methods + /// + /// Install a set of schema objects into the database + /// + /// + /// This is a transactional operation. + /// Also, note that although the names of schema objects are validated to not contain insecure characters, + /// the scripts that are installed are not checked. + /// + /// The schema group to install objects into + /// The list of schema objects to install + /// If object names are not unique + /// If schemaGroup is null + /// If objects is null + /// If any object fails to install + /// If a parameter contains an invalid SQL character + public string Install (string schemaGroup, SchemaObjectCollection objects) + { + return Install (schemaGroup, objects, false); + } + + public string Install (string schemaGroup, SchemaObjectCollection objects, bool rebuild) + { + return Install (schemaGroup, objects, rebuild ? RebuildMode.RebuildFull : RebuildMode.DetectChanges); + } + + public string Install (string schemaGroup, SchemaObjectCollection objects, RebuildMode rebuildMode) + { + _scripts = new StringBuilder (); + + // validate the arguments + if (schemaGroup == null) throw new ArgumentNullException ("schemaGroup"); + if (objects == null) throw new ArgumentNullException ("objects"); + + // get the list of objects + List schemaObjects = new List (objects); + ValidateSchemaObjects (schemaObjects); + for (int i = 0; i < objects.Count; i++) + objects[i].OriginalOrder = i; + + // sort the list of objects in installation order + schemaObjects.Sort (delegate (SchemaObject o1, SchemaObject o2) + { + int compare = o1.SchemaObjectType.CompareTo (o2.SchemaObjectType); + if (compare == 0) + compare = o1.OriginalOrder.CompareTo (o2.OriginalOrder); + if (compare == 0) + compare = String.Compare(o1.Name, o2.Name, StringComparison.OrdinalIgnoreCase); + return compare; + }); + + // the schema changes must be done in a transaction + // since we don't pool the connection, we need to end the transaction before closing the connection + try + { + using (TransactionScope transaction = new TransactionScope (TransactionScopeOption.Required, new TimeSpan (1, 0, 0, 0, 0))) + { + // open the connection + OpenConnection (); + + // make sure we have a schema registry + SchemaRegistry registry = new SchemaRegistry (_connection); + + // keep a list of all of the operations we need to perform + List dropObjects = new List (); + List addObjects = new List (); + List tableUpdates = new List (); + + // look through all of the existing objects in the registry + // create a delete instruction for all of the ones that should no longer be there + foreach (string objectName in registry.GetObjectNames (schemaGroup)) + { + SchemaObject schemaObject = schemaObjects.Find (delegate (SchemaObject o) + { + return (o.Name.ToUpperInvariant() == objectName.ToUpperInvariant()); + }); + if (schemaObject == null) + dropObjects.Add (objectName); + } + + // sort to drop in reverse dependency order + dropObjects.Sort (delegate (string o1, string o2) + { + int compare = -registry.GetObjectType (o1).CompareTo (registry.GetObjectType (o2)); + if (compare == 0) + compare = -registry.GetOriginalOrder(o1).CompareTo(registry.GetOriginalOrder(o2)); + if (compare == 0) + compare = -String.Compare(o1, o2, StringComparison.OrdinalIgnoreCase); + + return compare; + }); + + // find out if we need to add anything + foreach (SchemaObject schemaObject in schemaObjects) + { + // add any objects that aren't in the registry yet + if (!registry.Contains (schemaObject.Name)) + addObjects.Add (schemaObject); + } + + // see if there are any drops or modifications + bool hasChanges = dropObjects.Count != 0; + if (!hasChanges) + { + foreach (SchemaObject schemaObject in schemaObjects) + { + if (registry.Contains (schemaObject.Name) && + registry.GetSignature (schemaObject.Name) != schemaObject.Signature) + { + hasChanges = true; + break; + } + } + } + + // if there are changes, drop all of the easy items + // drop and re-add all of the easy items + if (hasChanges || (rebuildMode > RebuildMode.DetectChanges)) + { + for (int i = schemaObjects.Count - 1; i >= 0; i--) + { + SchemaObject schemaObject = schemaObjects [i]; + if (registry.Contains (schemaObject.Name) && + ( + IsEasyToModify (schemaObject.SchemaObjectType) || + (rebuildMode >= RebuildMode.RebuildSafe && CanRebuildSafely (schemaObject.SchemaObjectType)) || + (rebuildMode >= RebuildMode.RebuildFull && CanRebuild (schemaObject.SchemaObjectType)) + ) && + !dropObjects.Contains (schemaObject.Name)) + { + dropObjects.Add (schemaObject.Name); + addObjects.Add (schemaObject); + } + } + } + + // drop and re-add everything else, using the scripting engine + for (int i = schemaObjects.Count - 1; i >= 0; i--) + { + SchemaObject schemaObject = schemaObjects [i]; + + if (registry.Contains (schemaObject.Name) && + registry.GetSignature (schemaObject.Name) != schemaObject.Signature && + !IsEasyToModify (schemaObject.SchemaObjectType) && + !dropObjects.Contains (schemaObject.Name)) + ScheduleUpdate (dropObjects, addObjects, tableUpdates, schemaObject, true); + } + + // sort to add in dependency order + addObjects.Sort (delegate (SchemaObject o1, SchemaObject o2) + { + int compare = o1.SchemaObjectType.CompareTo (o2.SchemaObjectType); + if (compare == 0) + compare = o1.OriginalOrder.CompareTo (o2.OriginalOrder); + if (compare == 0) + compare = String.Compare (o1.Name, o2.Name, StringComparison.OrdinalIgnoreCase); + return compare; + }); + + // do the work + DropObjects (registry, dropObjects, addObjects); + UpdateTables (schemaGroup, registry, addObjects, tableUpdates); + CreateObjects (schemaGroup, registry, addObjects); + VerifyObjects (schemaObjects); + + // update the sigs on all of the records + foreach (SchemaObject o in schemaObjects) + registry.UpdateObject(o, schemaGroup); + + // commit the changes + registry.Update (); + transaction.Complete(); + } + } + finally + { + _connection.Dispose (); + } + + return _scripts.ToString (); + } + + public bool Diff (string schemaGroup, SchemaObjectCollection schemaObjects) + { + try + { + OpenConnection (); + SchemaRegistry registry = new SchemaRegistry (_connection); + + // drop and re-add everything else, using the scripting engine + foreach (SchemaObject schemaObject in schemaObjects) + { + // if the registry is missing the object, it's new, that's a diff + if (!registry.Contains (schemaObject.Name)) + return true; + + // if the signatures don't match, that's a diff + if (registry.GetSignature (schemaObject.Name) != schemaObject.Signature) + return true; + } + + // look through all of the existing objects in the registry + // create a delete instruction for all of the ones that should no longer be there + foreach (string objectName in registry.GetObjectNames (schemaGroup)) + { + SchemaObject schemaObject = schemaObjects.FirstOrDefault (delegate (SchemaObject o) + { + return (o.Name.ToUpperInvariant() == objectName.ToUpperInvariant()); + }); + if (schemaObject == null) + return true; + } + + // didn't detect differences + return false; + } + finally + { + _connection.Dispose (); + } + } + + private static bool IsEasyToModify (SchemaObjectType schemaObjectType) + { + switch (schemaObjectType) + { + case SchemaObjectType.View: + case SchemaObjectType.Function: + case SchemaObjectType.StoredProcedure: + case SchemaObjectType.Permission: + case SchemaObjectType.Trigger: + return true; + } + + return false; + } + + private static bool CanRebuildSafely(SchemaObjectType schemaObjectType) + { + if (IsEasyToModify (schemaObjectType)) + return true; + + switch (schemaObjectType) + { + case SchemaObjectType.Constraint: + case SchemaObjectType.ForeignKey: + return true; + } + + return false; + } + + private static bool CanRebuild(SchemaObjectType schemaObjectType) + { + if (CanRebuildSafely (schemaObjectType)) + return true; + + switch (schemaObjectType) + { + case SchemaObjectType.IndexedView: + case SchemaObjectType.Index: + case SchemaObjectType.PrimaryKey: + case SchemaObjectType.PrimaryXmlIndex: + case SchemaObjectType.SecondaryXmlIndex: + case SchemaObjectType.UserScript: + return true; + } + + return false; + } + + /// + /// Perform a dry run by running the install and rolling back + /// + public string DryRun (string schemaGroup, SchemaObjectCollection objects) + { + using (TransactionScope transaction = new TransactionScope (TransactionScopeOption.Required, new TimeSpan ())) + { + return Install (schemaGroup, objects); + } + } + + private StringBuilder _scripts; + + /// + /// Schedule an update by adding the appropriate delete, update and add records + /// + /// The list of tdrops + /// The list of adds + /// The list of table updates + /// The object to update + private void ScheduleUpdate (List dropObjects, List addObjects, List tableUpdates, SchemaObject schemaObject, bool handleDependencies) + { + // if the object is a table, we need to update it + if (schemaObject.SchemaObjectType == SchemaObjectType.Table) + tableUpdates.Add (schemaObject); + else + { + // not a table, so add a drop and insert + // put the add in before scripting the permissions so the permissions execute after the add + addObjects.Add (schemaObject); + if (handleDependencies) + { + switch (schemaObject.SchemaObjectType) + { + case SchemaObjectType.StoredProcedure: + StoredProcedure sp = _database.StoredProcedures [schemaObject.UnformattedName]; + if (sp != null) + ScriptPermissions (sp.Urn, addObjects); + break; + + case SchemaObjectType.Function: + UserDefinedFunction udf = _database.UserDefinedFunctions [schemaObject.UnformattedName]; + if (udf != null) + ScriptPermissions (udf.Urn, addObjects); + break; + + case SchemaObjectType.View: + View view = _database.Views [schemaObject.UnformattedName]; + if (view != null) + ScriptPermissions (view.Urn, addObjects); + break; + } + } + + // insert at the beginning so the higher level objects get dropped before their dependencies + dropObjects.Add (schemaObject.Name); + } + } + + private static void ValidateSchemaObjects (List schemaObjects) + { + // check for duplicate objects and invalid names + foreach (SchemaObject schemaObject in schemaObjects) + { + AssertValidSqlName (schemaObject.Name); + if (schemaObjects.FindAll (delegate (SchemaObject o) { return o.Name == schemaObject.Name; }).Count > 1) + throw new ArgumentException (String.Format (CultureInfo.InvariantCulture, Properties.Resources.DuplicateObjectName, schemaObject.Name)); + } + } + + private void DropObjects (SchemaRegistry registry, List dropObjects, List addObjects) + { + // drop objects + foreach (string objectName in dropObjects) + { + if (DroppingObject != null) + DroppingObject (this, new SchemaEventArgs (SchemaEventType.BeforeDrop, objectName)); + + // drop any table dependencies, if any + SchemaObjectType type = registry.GetObjectType (objectName); + switch (type) + { + case SchemaObjectType.UserDefinedType: + DropTypeDependencies(objectName, addObjects); + break; + + case SchemaObjectType.View: + DropViewDependencies (objectName, addObjects); + break; + + case SchemaObjectType.Table: + DropTableDepencencies(objectName, null, TableScriptOptions.IncludeTableModifiers | TableScriptOptions.AllXmlIndexes, true); + break; + + case SchemaObjectType.PrimaryKey: + DropTableDepencencies(SchemaObject.TableNameFromIndexName(objectName), addObjects, TableScriptOptions.AddAtEnd | TableScriptOptions.AllXmlIndexes, false); + break; + + case SchemaObjectType.PrimaryXmlIndex: + DropTableDepencencies(SchemaObject.TableNameFromIndexName(objectName), addObjects, TableScriptOptions.AddAtEnd | TableScriptOptions.SecondaryXmlIndexes, false); + break; + } + + SchemaObject.Drop (this, _connection, type, objectName); + registry.DeleteObject (objectName); + ResetScripter (); + } + } + + private void UpdateTables (string schemaGroup, SchemaRegistry registry, List addObjects, List tableUpdates) + { + foreach (SchemaObject schemaObject in tableUpdates) + { + if (UpdatingTable != null) + UpdatingTable (this, new SchemaEventArgs (SchemaEventType.BeforeTableUpdate, schemaObject)); + + DropTableDepencencies(schemaObject.Name, addObjects, TableScriptOptions.IncludeTableModifiers | TableScriptOptions.AllXmlIndexes, true); + + // signature has changed, so update the object + UpdateTable (_connection, schemaObject); + registry.UpdateObject (schemaObject, schemaGroup); + + if (UpdatedTable != null) + UpdatedTable (this, new SchemaEventArgs (SchemaEventType.AfterTableUpdate, schemaObject)); + } + } + + private void CreateObjects (string schemaGroup, SchemaRegistry registry, List addObjects) + { + // create objects + foreach (SchemaObject schemaObject in addObjects) + { + if (CreatingObject != null) + CreatingObject (this, new SchemaEventArgs (SchemaEventType.BeforeCreate, schemaObject)); + + schemaObject.Install(this); + + if (schemaObject.SchemaObjectType != SchemaObjectType.Script) + registry.UpdateObject (schemaObject, schemaGroup); + + if (CreatedObject != null) + CreatedObject (this, new SchemaEventArgs (SchemaEventType.AfterCreate, schemaObject)); + } + } + + /// + /// Verify that all of the objects that are supposed to be there really are... + /// + /// The objects + private void VerifyObjects (List schemaObjects) + { + // create objects + foreach (SchemaObject schemaObject in schemaObjects) + schemaObject.Verify (this, _connection); + } + + #region Table Update Methods + /// + /// Update a table object + /// + /// The SqlConnection to use + /// This creates a copy of the table data, updates the table, then copies the data back into the new table. + private void UpdateTable (SqlConnection connection, SchemaObject schemaObject) + { + // copy the table to a temp table and drop the old table + // NOTE: sp_rename can't be used because we're in a distributed transaction + SqlCommand command = new SqlCommand (); + command.Connection = connection; + //command.CommandText = String.Format (CultureInfo.InvariantCulture, "SELECT * INTO {0} FROM {1}; DROP TABLE {1}", TempTableName, schemaObject.Name); + command.CommandText = String.Format (CultureInfo.InvariantCulture, "sp_rename '{1}', '{0}'", TempTableName, schemaObject.Name); + command.CommandTimeout = 0; + command.ExecuteNonQuery (); + + // create the new table using the script provided + schemaObject.Install(this); + + if (!DoConvertTable (schemaObject, TempTableName, schemaObject.Name)) + { + // get the columns for the two tables + command.CommandText = String.Format (CultureInfo.InvariantCulture, @" + select c.*, type_name = t.name from sys.columns c join sys.types t on (c.system_type_id = t.system_type_id and c.user_type_id = t.user_type_id) where object_id = object_id ('{0}'); + select c.*, type_name = t.name from sys.columns c join sys.types t on (c.system_type_id = t.system_type_id and c.user_type_id = t.user_type_id) where object_id = object_id ('{1}'); + ", TempTableName, schemaObject.UnformattedName); + SqlDataAdapter adapter = new SqlDataAdapter (); + adapter.SelectCommand = command; + DataSet dataset = new DataSet (); + dataset.Locale = CultureInfo.InvariantCulture; + adapter.Fill (dataset); + + // find all of the fields that match from the old table to the new table + // for these fields, we'll preserve the data + StringBuilder newFields = new StringBuilder (); + StringBuilder oldFields = new StringBuilder (); + foreach (DataRow newRow in dataset.Tables[1].Rows) + { + string columnName = newRow["name"].ToString (); + + // don't map over timestamps and computed columns, they get done automatically + if (newRow["type_name"].ToString () == "timestamp") + continue; + if (Convert.ToInt32 (newRow ["is_computed"], CultureInfo.InvariantCulture) == 1) + continue; + + // find a matching oldRow + DataRow[] oldRows = dataset.Tables[0].Select (String.Format (CultureInfo.InvariantCulture, "name = '{0}'", columnName)); + + // map old fields to new fields + if (oldRows.Length == 1) + { + if (newFields.Length > 0) newFields.Append (","); + newFields.AppendFormat ("[{0}]", columnName); + + if (oldFields.Length > 0) oldFields.Append (","); + oldFields.AppendFormat ("[{0}]", columnName); + } + } + + // copy the data into the new table + command.CommandText = String.Format (CultureInfo.InvariantCulture, + @" IF OBJECTPROPERTY (OBJECT_ID('{0}'), 'TableHasIdentity') = 1 + SET IDENTITY_INSERT {0} ON; + INSERT INTO {0} ({1}) SELECT {2} FROM {3}; + IF OBJECTPROPERTY (OBJECT_ID('{0}'), 'TableHasIdentity') = 1 + SET IDENTITY_INSERT {0} OFF", + schemaObject.Name, newFields, oldFields, TempTableName); + command.ExecuteNonQuery (); + } + + // drop the temp table + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP TABLE {0}", TempTableName); + command.ExecuteNonQuery (); + } + + private bool DoConvertTable (SchemaObject table, string beforeTable, string afterTable) + { + if (OnConvertTable != null) + { + ConvertTableEventArgs ce = new ConvertTableEventArgs (SchemaEventType.ConvertTable, table, _connection, beforeTable, afterTable); + OnConvertTable (this, ce); + return ce.Converted; + } + return false; + } + + /// + /// The name of the temporary table to use + /// + public static readonly string TempTableName = "Insight_tmpTable"; + #endregion + + /// + /// Uninstall a schema group from the database. + /// + /// This is a transactional operation + /// The group to uninstall + /// If schemaGroup is null + /// If any object fails to uninstall + public void Uninstall (string schemaGroup) + { + // validate the arguments + if (schemaGroup == null) throw new ArgumentNullException ("schemaGroup"); + + // the schema changes must be done in a transaction + try + { + using (TransactionScope transaction = new TransactionScope ()) + { + // open the connection + OpenConnection (); + + // make sure we have a schema registry + SchemaRegistry registry = new SchemaRegistry (_connection); + + // sort the objects in drop order (reverse create order) + List names = registry.GetObjectNames (schemaGroup); + names.Sort (delegate (string n1, string n2) + { + return -registry.GetObjectType (n1).CompareTo (registry.GetObjectType (n2)); + }); + + // delete any objects that are in the specified schema group + foreach (string objectName in names) + { + if (DroppingObject != null) + DroppingObject (this, new SchemaEventArgs (SchemaEventType.BeforeDrop, objectName)); + + SchemaObjectType type = registry.GetObjectType (objectName); + if (type == SchemaObjectType.Table) + DropTableDepencencies(objectName, null, TableScriptOptions.IncludeTableModifiers, true); + SchemaObject.Drop (this, _connection, type, objectName); + registry.DeleteObject (objectName); + } + + // commit the changes + registry.Update (); + transaction.Complete(); + } + } + finally + { + _connection.Dispose (); + } + } + #endregion + + #region Scripting Methods + /// + /// Specifies how table scripting should be done + /// + [Flags] + enum TableScriptOptions + { + /// + /// Script the constraints on the table itself + /// + IncludeTableModifiers = 1 << 0, + + /// + /// Add the changes to the end of the list, not the beginning + /// + AddAtEnd = 1 << 1, + + /// + /// Are we scripting the existing table or a table referencing our table + /// + ScriptAnonymousConstraints = 1 << 2, + + /// + /// Script Primary Xml Indexes + /// + PrimaryXmlIndexes = 1 << 3, + + /// + /// Script Secondary Xml Indexes + /// + SecondaryXmlIndexes = 1 << 4, + + /// + /// All Xml Indexes + /// + AllXmlIndexes = PrimaryXmlIndexes | SecondaryXmlIndexes, + } + + /// + /// Drops all of the dependencies on a table so the table can be updated. + /// + /// The table to update + /// The list of addObjects, or null to not re-add dependencies + /// The list of updated being made to tables. Need to prevent duplicate changes + /// Options for scripting + /// Drops the dependencies and adds SchemaObjects to readd the dependencies later + private void DropTableDepencencies(string tableName, List addObjects, TableScriptOptions options, bool modifyingTable) + { + try + { + Table table = _database.Tables[SchemaObject.UnformatSqlName (tableName)]; + if (table == null) + return; + + DependencyTree tree = _scripter.DiscoverDependencies (new SqlSmoObject[] { table }, DependencyType.Children); + + // find all of the tables that refer to this table and drop all of the foreign keys + for (DependencyTreeNode dependent = tree.FirstChild.FirstChild; dependent != null; dependent = dependent.NextSibling) + { + if (dependent.Urn.Type == "Table") + { + // script the re-add of foreign keys, but not the tables themselves + _scripter.Options = new ScriptingOptions (ScriptOption.DriForeignKeys) - ScriptOption.PrimaryObject; + + // don't script anonymous constraints. + // if they are on the table, they will get created with the new table + // if they are not on the table, they should go away + options &= ~TableScriptOptions.ScriptAnonymousConstraints; + + DropAndReAdd (dependent.Urn, addObjects, options); + } + else + if (modifyingTable && dependent.Urn.Type == "View") + { + DropViewDependencies (dependent.Urn, addObjects); + DropAndReAdd (dependent.Urn, addObjects, options); + } + } + + // handle xml indexes separately + if ((options & TableScriptOptions.AllXmlIndexes) != 0) + { + // drop all of the permissions, constraints, etc. on this table, but not the table itself + _scripter.Options = new ScriptingOptions (); + _scripter.Options.XmlIndexes = true; + _scripter.Options -= ScriptOption.PrimaryObject; + + TableScriptOptions xmlOptions = options; + if ((options & TableScriptOptions.PrimaryXmlIndexes) != 0) + xmlOptions &= ~TableScriptOptions.AddAtEnd; + DropAndReAdd (tree.FirstChild.Urn, addObjects, xmlOptions); + } + + // drop the objects on the table itself + if ((options & TableScriptOptions.IncludeTableModifiers) != 0) + { + options &= ~TableScriptOptions.ScriptAnonymousConstraints; + + // drop all of the permissions, constraints, etc. on this table, but not the table itself + _scripter.Options = new ScriptingOptions (); + _scripter.Options.DriAll = true; + _scripter.Options.NonClusteredIndexes = true; + _scripter.Options.ClusteredIndexes = true; + _scripter.Options -= ScriptOption.PrimaryObject; + DropAndReAdd (tree.FirstChild.Urn, addObjects, options); + + // script the permissions on the table + ScriptPermissions (tree.FirstChild.Urn, addObjects); + + _scripter.Options = new ScriptingOptions (ScriptOption.Triggers); + DropAndReAdd (tree.FirstChild.Urn, addObjects, options); + } + } + finally + { + _connection.ChangeDatabase (_databaseName); + } + } + + private void DropViewDependencies (Urn urn, List addObjects) + { + try + { + DependencyTree tree = _scripter.DiscoverDependencies (new Urn [] { urn }, DependencyType.Children); + for (DependencyTreeNode dependent = tree.FirstChild.FirstChild; dependent != null; dependent = dependent.NextSibling) + { + // for each child object, script it and its permissions + _scripter.Options = new ScriptingOptions (); + _scripter.Options.DriAll = true; + _scripter.Options.Permissions = true; + DropAndReAdd (dependent.Urn, addObjects, TableScriptOptions.AddAtEnd); + } + } + finally + { + _connection.ChangeDatabase (_databaseName); + } + } + + private void DropViewDependencies (string viewName, List addObjects) + { + View view = _database.Views [SchemaObject.UnformatSqlName (viewName)]; + if (view == null) + return; + + DropViewDependencies (view.Urn, addObjects); + } + + private void DropTypeDependencies (string name, List addObjects) + { + var type = _database.UserDefinedTableTypes[SchemaObject.UnformatSqlName(name)]; + if (type == null) + return; + + DropTypeDependencies(type.Urn, addObjects); + } + + private void DropTypeDependencies (Urn urn, List addObjects) + { + try + { + DependencyTree tree = _scripter.DiscoverDependencies(new Urn[] { urn }, DependencyType.Children); + for (DependencyTreeNode dependent = tree.FirstChild.FirstChild; dependent != null; dependent = dependent.NextSibling) + { + // for each child object, script it and its permissions + _scripter.Options = new ScriptingOptions(); + _scripter.Options.DriAll = true; + _scripter.Options.Permissions = true; + DropAndReAdd(dependent.Urn, addObjects, TableScriptOptions.AddAtEnd); + } + } + finally + { + _connection.ChangeDatabase(_databaseName); + } + } + + /// + /// Script the permissions on an object and save the script to add the permissions back later + /// + /// The object to drop + /// The list of addObjects, or null to not re-add dependencies + private void ScriptPermissions (Urn urn, List addObjects) + { + if (addObjects == null) + return; + + // generate the script and add it if we've generated anything + _scripter.Options = new ScriptingOptions (ScriptOption.Permissions); + _scripter.Options.IncludeIfNotExists = true; + _scripter.Options.ScriptDrops = false; + string addScript = GenerateScript (urn); + + // scripting permissions on a function returns the body + if (addScript.IndexOf("CREATE FUNCTION", StringComparison.OrdinalIgnoreCase) >= 0) + addScript = ""; + + if (addScript.Length > 0) + addObjects.Add (new SchemaObject (SchemaObjectType.Permission, "Scripted Permissions", addScript)); + + // smo switches to master, so switch back to our db + _connection.ChangeDatabase (_databaseName); + } + + /// + /// Drop an object and generate the script to re-add it + /// + /// The object to drop + /// The list of addObjects, or null to not re-add dependencies + /// Options for scripting + private void DropAndReAdd (Urn urn, List addObjects, TableScriptOptions options) + { + // generate the script to readd + if (addObjects != null) + { + _scripter.Options.ScriptDrops = false; + _scripter.Options.IncludeIfNotExists = true; + string addScript = GenerateScript (urn); + + // unnamed primary keys and constraints need to not be auto-added, since they must be built into the table + if ((options & TableScriptOptions.ScriptAnonymousConstraints) != 0) + addScript = _anonymousReferenceRegex.Replace (addScript, "IF 0=1 $0"); + else + { + addScript = _anonymousRegex.Replace (addScript, "IF 0=1 $0"); + addScript = _anonymousDefaultRegex.Replace (addScript, "IF 0=1 $0"); + } + + // if the database has autostatistics, then skip all statistics + addScript = _statisticsRegex.Replace (addScript, ""); + + // create triggers must be the first statement in the batch + int pos = addScript.IndexOf("CREATE TRIGGER", StringComparison.OrdinalIgnoreCase); + if (pos >= 0) + addScript = addScript.Substring (pos); + + // remove primary xml indexes if we don't need them + if ((options & TableScriptOptions.PrimaryXmlIndexes) == 0) + addScript = _primaryXmlIndex.Replace (addScript, "IF 0=1 $0"); + + if (addScript.Length > 0) + { + SchemaObject newObject = new SchemaObject (SchemaObjectType.Script, "Scripted Dependencies", addScript); + if ((options & TableScriptOptions.AddAtEnd) != 0) + addObjects.Add (newObject); + else + addObjects.Insert (0, newObject); + } + } + + // script the drop of everything + _scripter.Options.ScriptDrops = true; + _scripter.Options.IncludeIfNotExists = true; + string dropScript = GenerateScript (urn); + + // the scripter should not be scripting the table drop, so we have to comment it out + // note that the !PrimaryObject option above works for the create script + SqlSmoObject smo = _scripter.Server.GetSmoObject (urn); + Table table = smo as Table; + if (table != null) + dropScript = _dropTableRegex.Replace (dropScript, "SELECT 1"); + + // smo switches to master, so switch back to our db + _connection.ChangeDatabase (_databaseName); + + if (!String.IsNullOrWhiteSpace(dropScript)) + ExecuteNonQuery (dropScript); + + ResetScripter (); + } + + /// + /// Matches a DROP TABLE statement + /// + private static readonly Regex _dropTableRegex = new Regex (String.Format (CultureInfo.InvariantCulture, @"DROP \s+ TABLE \s+ {0}", SchemaObject.SqlNameExpression), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Matches a CREATE STATISTICS statement + /// + private static readonly Regex _statisticsRegex = new Regex (String.Format (CultureInfo.InvariantCulture, @"CREATE\s+STATISTICS\s+(?{0})\s+ON\s+{0}\({0}(,\s*{0})*\)", SchemaObject.SqlNameExpression), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Matches anonymous keys and check constraints + /// + private static readonly Regex _anonymousRegex = new Regex (String.Format (CultureInfo.InvariantCulture, @"ALTER\s+TABLE\s+{0}(\s+WITH\s+CHECK)?\s+ADD\s+(((PRIMARY|FOREIGN)\s+KEY(\s+(NON)?CLUSTERED)?)|(CHECK))", SchemaObject.SqlNameExpression), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Matches anonymous keys and check constraints on table references. + /// + /// We need to script anonymous FKs on reference tables, because we drop them + private static readonly Regex _anonymousReferenceRegex = new Regex (String.Format (CultureInfo.InvariantCulture, @"ALTER\s+TABLE\s+{0}(\s+WITH\s+CHECK)?\s+ADD\s+(((PRIMARY)\s+KEY(\s+(NON)?CLUSTERED)?)|(CHECK))", SchemaObject.SqlNameExpression), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Matches anonymous keys and check constraints on table references. + /// + /// We need to script anonymous FKs on reference tables, because we drop them + private static readonly Regex _anonymousDefaultRegex = new Regex (String.Format (CultureInfo.InvariantCulture, @"ALTER\s+TABLE\s+{0}\s+ADD\s+DEFAULT\s+\([^\]]+]", SchemaObject.SqlNameExpression), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Matches primary xml index + /// + private static readonly Regex _primaryXmlIndex = new Regex (@"CREATE\s+PRIMARY\s+XML\s+INDEX", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Generate a script for a database object. + /// + /// The object to script + /// String containg the script + private string GenerateScript (Urn urn) + { + UrnCollection urns = new UrnCollection (); + urns.Add (urn); + + return GenerateScript (urns); + } + + /// + /// Generate a script for a database object. + /// + /// The objects to script + /// String containg the script + private string GenerateScript (UrnCollection urns) + { + _scripter.Options.ContinueScriptingOnError = true; + + // script the the list of objects passed in + StringCollection scriptCollection = _scripter.Script (urns); + StringBuilder sb = new StringBuilder (); + foreach (string s in scriptCollection) + sb.AppendLine (s); + + return sb.ToString(); + } + #endregion + + #region Private Members + /// + /// Opens the connection to the database and get a new command + /// + /// The connection to the database + private SqlConnection OpenConnection () + { + // connect to the database + _connection = new SqlConnection (_connectionString); + _connection.Open (); + _command = new SqlCommand (); + _command.Connection = _connection; + + ResetScripter (); + + _connection.ChangeDatabase (_databaseName); + return _connection; + } + + /// + /// Reset the scripter so the objects aren't cached + /// + private void ResetScripter () + { + // prepare the SMO objects + ServerConnection connection = new ServerConnection (_connection); + Server server = new Server (connection); + _database = server.Databases[_databaseName]; + _scripter = new Scripter (server); + } + + /// + /// Execute sql directly + /// + /// The sql to execute + internal void ExecuteNonQuery (string sql) + { + // append the contents of the command + if (_scripts != null) + { + _scripts.AppendLine (sql); + _scripts.AppendLine ("GO"); + } + + // never time out an upgrade + _command.CommandTimeout = 0; + _command.CommandText = sql; + _command.ExecuteNonQuery (); + + ResetScripter (); + } + + /// + /// Check to see if the database exists + /// + /// True if the database already exists + private bool DatabaseExists () + { + _command = new SqlCommand ("SELECT COUNT (*) FROM master..sysdatabases WHERE name = @DatabaseName", _connection); + _command.Parameters.AddWithValue ("@DatabaseName", _databaseName); + int count = (int)_command.ExecuteScalar (); + _command.Parameters.Clear (); + + return count > 0; + } + + /// + /// The connection string to the database + /// + private string _connectionString; + + /// + /// The name of the database to edit + /// + private string _databaseName; + + /// + /// The command to use to connect to the database + /// + private SqlCommand _command; + + /// + /// The current connection to the database + /// + private SqlConnection _connection; + + /// + /// The SMO object for scripting + /// + private Scripter _scripter; + + /// + /// The SMO object for connecting to the databas + /// + private Microsoft.SqlServer.Management.Smo.Database _database; + #endregion + + #region Security Methods + /// + /// Makes sure that a name of a schema object does not contain any insecure characters. + /// + /// The name to check + /// If a parameter contains an invalid SQL character + /// If the name is null + private static void AssertValidSqlName(string name) + { + if (name == null) + throw new ArgumentNullException ("name"); + if (name.Length == 0 || name.IndexOfAny (_insecureSqlChars) >= 0) + throw new ArgumentException (String.Format (CultureInfo.CurrentCulture, Resources.InvalidSqlObjectName, name)); + } + + /// + /// Characters that could cause bad sql things to happen + /// + /// + /// - can create a comment + /// ; can end a statement + /// ' can end a string + /// " can end a string + /// + private static readonly char[] _insecureSqlChars = new char[] { '-', ';', '\'' }; + #endregion + + #region Import Method + /// + /// Imports an existing database into the schema registry + /// + /// The name of the schema group to script to + public void Import (string schemaGroup) + { + using (TransactionScope transaction = new TransactionScope (TransactionScopeOption.Required, new TimeSpan ())) + { + // open the connection + OpenConnection (); + + // make sure we have a schema registry + SchemaRegistry registry = new SchemaRegistry (_connection); + + // get all of the objects in the current database + _command.CommandText = @" + SELECT o.name, o.type, p.name + FROM sys.objects o + LEFT JOIN sys.objects p ON (o.parent_object_id = p.object_id) + LEFT JOIN sys.default_constraints df ON (o.object_id = df.object_id) + WHERE o.is_ms_shipped = 0 + -- don't import anonymous defaults + AND (df.is_system_named IS NULL OR df.is_system_named = 0) + AND o.Name NOT LIKE '%Insight_SchemaRegistry%' + UNION + select i.name, 'IX', o.name + FROM sys.indexes i + JOIN sys.objects o ON (i.object_id = o.object_id) + WHERE o.is_ms_shipped = 0 AND i.type_desc <> 'HEAP' and is_primary_key = 0 and is_unique_constraint = 0"; + using (SqlDataReader reader = _command.ExecuteReader ()) + { + while (reader.Read ()) + { + SchemaObjectType type; + + string name = String.Format(CultureInfo.InvariantCulture, "[{0}]", reader.GetString(0)); + string sqlType = reader.GetString (1); + + switch (sqlType.Trim()) + { + case "U": + type = SchemaObjectType.Table; + break; + + case "P": + type = SchemaObjectType.StoredProcedure; + break; + + case "V": + type = SchemaObjectType.View; + break; + + case "FN": + case "TF": + type = SchemaObjectType.Function; + break; + + case "D": + case "UQ": + case "C": + type = SchemaObjectType.Constraint; + name = String.Format(CultureInfo.InvariantCulture, "[{0}].[{1}]", reader.GetString(2), reader.GetString(0)); + break; + + case "PK": + type = SchemaObjectType.PrimaryKey; + name = String.Format(CultureInfo.InvariantCulture, "[{0}].[{1}]", reader.GetString(2), reader.GetString(0)); + break; + + case "F": + type = SchemaObjectType.ForeignKey; + name = String.Format(CultureInfo.InvariantCulture, "[{0}].[{1}]", reader.GetString(2), reader.GetString(0)); + break; + + case "IX": + type = SchemaObjectType.Index; + name = String.Format(CultureInfo.InvariantCulture, "[{0}].[{1}]", reader.GetString(2), reader.GetString(0)); + break; + + case "SQ": + // query notification, skip + continue; + + default: + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot import object {0} of type {1}", name, sqlType)); + } + + SchemaObject schemaObject = new SchemaObject (type, name, ""); + registry.UpdateObject (schemaObject, schemaGroup); + } + } + + registry.Update (); + + transaction.Complete (); + } + } + #endregion + + #region Event Handling + /// + /// Called when a table is being converted. + /// + /// + /// When the event is fired, the table data exists in the before table. + /// If the event handler converts the data, return true. + /// Return false to allow the installer to use the default conversion. + /// + public event EventHandler OnConvertTable; + + /// + /// Called before a table is updated + /// + public event EventHandler UpdatingTable; + + /// + /// Called after a table is updated + /// + public event EventHandler UpdatedTable; + + /// + /// Called before a SchemaObject is created + /// + public event EventHandler CreatingObject; + + /// + /// Called after a SchemaObject is created + /// + public event EventHandler CreatedObject; + + /// + /// Called before a SchemaObject is dropped + /// + public event EventHandler DroppingObject; + + /// + /// Called when an object is missing + /// + public event EventHandler MissingObject; + internal void OnMissingObject (object sender, SchemaEventArgs e) + { + if (MissingObject != null) + MissingObject (sender, e); + } + #endregion + + public void Dispose() + { + if (_command != null) + { + _command.Dispose(); + _command = null; + } + if (_connection != null) + { + _connection.Dispose(); + _connection = null; + } + } + } + #endregion + + /// + /// Determines how aggressively to rebuild the database + /// + public enum RebuildMode + { + /// + /// Only build if there are changes + /// + DetectChanges, + + /// + /// Rebuild anything that can be safely updated without taking a long time + /// + RebuildSafe, + + /// + /// Rebuild anything that can be rebuilt + /// + RebuildFull + } +} diff --git a/Insight.Database.Schema/SchemaObject.cs b/Insight.Database.Schema/SchemaObject.cs new file mode 100644 index 0000000..9cf2527 --- /dev/null +++ b/Insight.Database.Schema/SchemaObject.cs @@ -0,0 +1,669 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +#endregion + +namespace Insight.Database.Schema +{ + #region SchemaObject Class + public sealed class SchemaObject + { + #region Constructors + /// + /// Constructs a SchemaObject of the given type, name, and sql script + /// + /// The type of the SchemaObject + /// The name of the SchemaObject + /// The SQL script for the SchemaObject + /// The type and name must match the SQL script. + /// If name or sql is null + public SchemaObject (SchemaObjectType type, string name, string sql) + { + if (name == null) throw new ArgumentNullException ("name"); + if (sql == null) throw new ArgumentNullException ("sql"); + + + _type = type; + _name = name; + _sql = sql; + } + + /// + /// Create a SchemaObject from a sql snippet, attempting to detect the type and name of the object + /// + /// The sql to parse + /// If the SQL cannot be parsed + public SchemaObject (string sql) + { + _sql = sql; + ParseSql (); + } + #endregion + + #region Properties + /// + /// The sql script for the object + /// + /// The sql script for the object + /// If sql cannot be parsed + public string Sql + { + get { return _sql; } + set + { + if (value == null) throw new ArgumentNullException ("value"); + _sql = value; + ParseSql (); + } + } + private string _sql; + + /// + /// The name of the SchemaObject + /// + /// The name of the SchemaObject + public string Name { get { return _name; } } + private string _name; + + /// + /// The name without formatting pieces + /// + /// + internal string UnformattedName { get { return UnformatSqlName (_name); } } + + /// + /// The type of the SchemaObject + /// + /// The type of the SchemaObject + public SchemaObjectType SchemaObjectType { get { return _type; } } + private SchemaObjectType _type; + + /// + /// The signature of the SchemaObject + /// + /// + public string Signature { get { return CalculateSignature (Sql); } } + + /// + /// The order in which the script was added to the schema + /// + /// The original order + /// Used for ties within the same type of object + internal int OriginalOrder + { + get { return _originalOrder; } + set { _originalOrder = value; } + } + private int _originalOrder; + #endregion + + #region Install/Uninstall Methods + /// + /// Install the object into the database + /// + /// The database connection to use + internal void Install(SchemaInstaller installer) + { + if (Sql.Length > 0) + { + try + { + installer.ExecuteNonQuery (Sql); + } + catch (Exception e) + { + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot create SQL object {0}: {1}", Name, e.Message), e); + } + } + } + + + internal void Verify (SchemaInstaller installer, SqlConnection connection) + { + SqlCommand command = new SqlCommand (); + command.Connection = connection; + command.CommandTimeout = 0; + + switch (SchemaObjectType) + { + default: + case SchemaObjectType.UserPreScript: + case SchemaObjectType.Unused: + case SchemaObjectType.Script: + case SchemaObjectType.UserScript: + case SchemaObjectType.Permission: + return; + + case SchemaObjectType.Role: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.database_principals WHERE name = '{0}' AND type = 'R'", Regex.Match(Name, @"\[ROLE (?[^\]]*)\]").Groups["name"].Value); + break; + + case SchemaObjectType.User: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.database_principals WHERE name = '{0}' AND type = 'U'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Login: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.server_principals WHERE name = '{0}' AND (type = 'U' OR type = 'S')", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Schema: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.schemas WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Certificate: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.certificates WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.MasterKey: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.symmetric_keys WHERE name = '{0}'", "##MS_DatabaseMasterKey##"); + break; + + case SchemaObjectType.SymmetricKey: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.symmetric_keys WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Service: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.services WHERE name = '{0}'", Regex.Match(Name, @"\[SERVICE (?[^\]]*)\]").Groups["name"].Value); + break; + + case SchemaObjectType.Queue: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.service_queues WHERE name = '{0}'", Regex.Match(Name, @"\[QUEUE (?[^\]]*)\]").Groups["name"].Value); + break; + + case SchemaObjectType.UserDefinedType: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.types WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.PartitionFunction: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.partition_functions WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.PartitionScheme: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.partition_schemes WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Table: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.tables WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.View: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.views WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.StoredProcedure: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.procedures WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.PrimaryKey: + case SchemaObjectType.Index: + case SchemaObjectType.PrimaryXmlIndex: + case SchemaObjectType.SecondaryXmlIndex: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.indexes WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Trigger: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.triggers WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.ForeignKey: + case SchemaObjectType.Constraint: + case SchemaObjectType.Function: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.objects WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.MessageType: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.service_message_types WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.Contract: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.service_contracts WHERE name = '{0}'", UnformatSqlName(Name)); + break; + + case SchemaObjectType.BrokerPriority: + command.CommandText = String.Format(CultureInfo.InvariantCulture, "SELECT COUNT (*) FROM sys.conversation_priorities WHERE name = '{0}'", UnformatSqlName(Name)); + break; + } + + // execute the query + int exists = Convert.ToInt32(command.ExecuteScalar(), CultureInfo.InvariantCulture); + + // if it doesn't exist, install it + if (exists == 0) + { + installer.OnMissingObject (this, new SchemaEventArgs (SchemaEventType.MissingObject, this)); + Install(installer); + } + } + + + /// + /// Drop an object from the database + /// + /// The Sql connection to use + /// The type of the object + /// The name of the object + internal static void Drop (SchemaInstaller installer, SqlConnection connection, SchemaObjectType type, string objectName) + { + SqlCommand command = new SqlCommand (); + command.Connection = connection; + + string[] split; + string tableName; + switch (type) + { + default: + // don't need to drop it (e.g. scripts) + return; + + case SchemaObjectType.Table: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP TABLE {0}", objectName); + break; + case SchemaObjectType.UserDefinedType: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP TYPE {0}", objectName); + break; + case SchemaObjectType.MasterKey: + // don't drop as this could be a loss of data + command.CommandText = "SELECT NULL"; + break; + case SchemaObjectType.Certificate: + // don't drop as this could be a loss of data + command.CommandText = "SELECT NULL"; + break; + case SchemaObjectType.SymmetricKey: + // don't drop as this could be a loss of data + command.CommandText = "SELECT NULL"; + break; + case SchemaObjectType.Index: + case SchemaObjectType.PrimaryXmlIndex: + case SchemaObjectType.SecondaryXmlIndex: + split = objectName.Split ('.'); + tableName = split[split.Length - 2]; + string indexName = split[split.Length - 1]; + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP INDEX {1} ON {0}", tableName, indexName); + break; + case SchemaObjectType.PrimaryKey: + case SchemaObjectType.Constraint: + case SchemaObjectType.ForeignKey: + // need to drop constraints by table and constraint name + split = objectName.Split ('.'); + tableName = split[split.Length - 2]; + string constraintName = split[split.Length - 1]; + command.CommandText = String.Format (CultureInfo.InvariantCulture, "ALTER TABLE {0} DROP CONSTRAINT {1}", tableName, constraintName); + break; + case SchemaObjectType.IndexedView: + case SchemaObjectType.View: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP VIEW {0}", objectName); + break; + case SchemaObjectType.Function: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP FUNCTION {0}", objectName); + break; + case SchemaObjectType.StoredProcedure: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP PROCEDURE {0}", objectName); + break; + case SchemaObjectType.Permission: + // revoke a permission by replacing GRANT with REVOKE in the name + command.CommandText = "REVOKE " + objectName; + return; + case SchemaObjectType.Trigger: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP TRIGGER {0}", objectName); + break; + case SchemaObjectType.User: + case SchemaObjectType.Login: + case SchemaObjectType.Schema: + case SchemaObjectType.Role: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP {0}", objectName); + break; + case SchemaObjectType.UserScript: + case SchemaObjectType.UserPreScript: + // can't clean up user scripts + command.CommandText = "SELECT NULL"; + break; + case SchemaObjectType.PartitionScheme: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP PARTITION SCHEME {0}", objectName); + break; + case SchemaObjectType.PartitionFunction: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP PARTITION FUNCTION {0}", objectName); + break; + case SchemaObjectType.Queue: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP QUEUE {0}", UnformatSqlName (objectName).Split (new char[] {' '}, 2) [1]); + break; + case SchemaObjectType.Service: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP SERVICE {0}", UnformatSqlName (objectName).Split (new char [] { ' ' }, 2) [1]); + break; + case SchemaObjectType.MessageType: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP MESSAGE TYPE {0}", objectName); + break; + case SchemaObjectType.Contract: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP CONTRACT {0}", objectName); + break; + case SchemaObjectType.BrokerPriority: + command.CommandText = String.Format (CultureInfo.InvariantCulture, "DROP BROKER PRIORITY {0}", objectName); + break; + } + + try + { + installer.ExecuteNonQuery (command.CommandText); + } + catch (SqlException e) + { + Console.WriteLine ("WARNING: {0}", e.Message); + } + } + #endregion + + #region Signature Methods + /// + /// Calculate the signature of a string as a hash code + /// + /// The string to hash + /// The hash code for the string + /// This is used to detect changes to a schema object + private static string CalculateSignature (string s) + { + // Convert the string into an array of bytes. + byte[] bytes = new UnicodeEncoding ().GetBytes (s); + + // Create a new instance of SHA1Managed to create the hash value. + SHA1Managed shHash = new SHA1Managed (); + + // Create the hash value from the array of bytes. + byte[] hashValue = shHash.ComputeHash (bytes); + + // return the hash as a string + return Convert.ToBase64String (hashValue); + } + #endregion + + #region Parsing Methods + /// + /// Parse the SQL to determine the type and name + /// + private bool ParseSql () + { + List matches = new List (); + + // let all of the parsers try to guess the type + foreach (SqlParser parser in _parserList) + { + if (parser.Match (_sql)) + matches.Add (parser); + } + + // find the earliest match + matches.Sort (delegate (SqlParser p1, SqlParser p2) + { + SchemaObjectType type1; + int pos1; + p1.Match (_sql, out type1, out _name, out pos1); + + SchemaObjectType type2; + int pos2; + p2.Match (_sql, out type2, out _name, out pos2); + + // stick unused types as the last resort + if (type1 == SchemaObjectType.Unused) + return 1; + if (type2 == SchemaObjectType.Unused) + return -1; + + int compare = pos1.CompareTo (pos2); + if (compare == 0) + compare = type1.CompareTo (type2); + + return compare; + }); + + // we can't determine the type, so we have to quit + if (matches.Count == 0) + throw new SchemaParsingException (Properties.Resources.CannotDetermineScriptType, _sql); + + // use the earliest match + int pos; + matches [0].Match (_sql, out _type, out _name, out pos); + + // this is just junk stuff that SQL inserts + if (_type == SchemaObjectType.Unused) + return false; + + return true; + } + + /// + /// The list of sql parsers + /// + private static SqlParserList _parserList = new SqlParserList(); + + #region SqlParserClass + /// + /// A parser that detects the type and name of a sql object from a script + /// + class SqlParser + { + /// + /// Create a parser that detects a type from a pattern + /// + /// The type represented by the pattern + /// The pattern to detect + public SqlParser (SchemaObjectType type, string pattern) + { + _type = type; + _regex = new Regex (pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled); + _result = "$1"; + } + + /// + /// Create a parser that detects a type from a pattern + /// + /// The type represented by the pattern + /// The pattern to detect + /// The string used to generate the resulting name + public SqlParser (SchemaObjectType type, string pattern, string result) + { + _type = type; + _regex = new Regex (pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled); + _result = result; + } + + /// + /// Attempts to match the sql to the pattern. If successful, sets the type and name + /// + /// The sql to parse + /// If matched, updated to the current type + /// If matched, updated to the name + /// + public bool Match (string sql, out SchemaObjectType type, out string name, out int position) + { + Match match = _regex.Match (sql); + if (match.Success) + { + type = _type; + + // format the sql name so we don't have to worry about what people type + name = match.Result (_result); + if (type != SchemaObjectType.Permission) + { + string [] pieces = name.Split (_sqlNameDivider); + name = ""; + for (int i = pieces.Length - _result.Split (_sqlNameDivider).Length; i < pieces.Length; i++) + { + if (name.Length > 0) + name += "."; + name += FormatSqlName (pieces [i]); + } + } + + position = match.Index; + return true; + } + + type = SchemaObjectType.Unused; + name = null; + position = -1; + + return false; + } + + /// + /// Attempts to match the sql to the pattern + /// + /// + /// + public bool Match (string sql) + { + return _regex.Match (sql).Success; + } + + /// + /// Compare the parser by type + /// + /// Parser to compare + /// Parser to compare + /// The sort order + public static int CompareByType (SqlParser p1, SqlParser p2) { return p1._type.CompareTo (p2._type); } + + /// + /// The expression pattern to match + /// + private Regex _regex; + + /// + /// The corresponding object type + /// + private SchemaObjectType _type; + + /// + /// The string used to generate the result from the match + /// + private string _result; + } + #endregion + + #region SqlParserList Class + /// + /// The list of SqlParser codes + /// + class SqlParserList : List + { + /// + /// Create a list of SqlParsers + /// + public SqlParserList () + { + Add (new SqlParser (SchemaObjectType.IndexedView, String.Format (CultureInfo.InvariantCulture, @"^\s*--\s*INDEXEDVIEW\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.UserPreScript, String.Format (CultureInfo.InvariantCulture, @"^\s*--\s*PRESCRIPT\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.UserScript, String.Format (CultureInfo.InvariantCulture, @"^\s*--\s*SCRIPT\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.UserDefinedType, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+TYPE\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.UserDefinedType, String.Format (CultureInfo.InvariantCulture, @"EXEC(UTE)?\s+sp_addtype\s+'?(?{0})'?", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.MasterKey, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+MASTER\s+KEY\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.Certificate, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+CERTIFICATE\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.SymmetricKey, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+SYMMETRIC\s+KEY\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.PartitionFunction, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+PARTITION\s+FUNCTION\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.PartitionScheme, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+PARTITION\s+SCHEME\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.MessageType, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+MESSAGE TYPE\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.Contract, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+CONTRACT\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.BrokerPriority, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+BROKER\s+PRIORITY\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.Queue, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+QUEUE\s+(?{0})", SqlNameExpression), "QUEUE $1")); + Add (new SqlParser (SchemaObjectType.Service, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+SERVICE\s+(?{0})", SqlNameExpression), "SERVICE $1")); + Add (new SqlParser (SchemaObjectType.Table, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+TABLE\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.Trigger, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+TRIGGER\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.Index, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+(UNIQUE\s+)?(((CLUSTERED)|(NONCLUSTERED))\s+)?INDEX\s+(?{0})\s+ON\s+(?{0})", SqlNameExpression), "$2.$1")); + Add (new SqlParser (SchemaObjectType.View, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+VIEW\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.StoredProcedure, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+PROC(EDURE)?\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.Permission, String.Format (CultureInfo.InvariantCulture, @"GRANT\s+(?{0})\s+ON\s+(?{0})\s+TO\s+(?{0})", SqlNameExpression), "$1 ON $2 TO $3")); + Add(new SqlParser(SchemaObjectType.PrimaryKey, String.Format(CultureInfo.InvariantCulture, @"ALTER\s+TABLE\s+(?{0})\s+(WITH\s+(NO)?CHECK\s+)?ADD\s+CONSTRAINT\s*\(?(?{0})\)?\s+PRIMARY\s+", SqlNameExpression), "$1.$2")); + Add(new SqlParser(SchemaObjectType.ForeignKey, String.Format(CultureInfo.InvariantCulture, @"ALTER\s+TABLE\s+(?{0})\s+(WITH\s+(NO)?CHECK\s+)?ADD\s+CONSTRAINT\s*\(?(?{0})\)?\s+FOREIGN\s+KEY\s*\(?(?{0})\)?", SqlNameExpression), "$1.$2")); + Add(new SqlParser(SchemaObjectType.Constraint, String.Format(CultureInfo.InvariantCulture, @"ALTER\s+TABLE\s+(?{0})\s+(WITH\s+(NO)?CHECK\s+)?ADD\s+CONSTRAINT\s*\(?(?{0})\)?", SqlNameExpression), "$1.$2")); + Add (new SqlParser (SchemaObjectType.Function, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+FUNCTION\s+(?{0})", SqlNameExpression))); + Add (new SqlParser (SchemaObjectType.PrimaryXmlIndex, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+PRIMARY\s+XML\s+INDEX\s+(?{0})\s+ON\s+(?{0})", SqlNameExpression), "$2.$1")); + Add (new SqlParser (SchemaObjectType.SecondaryXmlIndex, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+XML\s+INDEX\s+(?{0})\s+ON\s+(?{0})", SqlNameExpression), "$2.$1")); + Add (new SqlParser (SchemaObjectType.Login, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+LOGIN\s+(?{0})", SqlNameExpression), "LOGIN $1")); + Add (new SqlParser (SchemaObjectType.User, String.Format (CultureInfo.InvariantCulture, @"CREATE\s+USER\s+(?{0})", SqlNameExpression), "USER $1")); + Add (new SqlParser (SchemaObjectType.Role, String.Format(CultureInfo.InvariantCulture, @"CREATE\s+ROLE\s+(?{0})", SqlNameExpression), "ROLE $1")); + Add (new SqlParser (SchemaObjectType.Schema, String.Format(CultureInfo.InvariantCulture, @"CREATE\s+SCHEMA\s+(?{0})", SqlNameExpression), "SCHEMA $1")); + Add(new SqlParser(SchemaObjectType.Unused, String.Format(CultureInfo.InvariantCulture, @"SET\s+ANSI_NULLS", SqlNameExpression), null)); + Add(new SqlParser(SchemaObjectType.Unused, String.Format(CultureInfo.InvariantCulture, @"SET\s+QUOTED_IDENTIFIER", SqlNameExpression), null)); + + Sort(new Comparison(SqlParser.CompareByType)); + } + } + + /// + /// Matches a SQL name in the form [a].[b].[c], or "a"."b"."c" or a.b.c (or any combination) + /// Also: TYPE :: sqlname for global scoping + /// + internal const string SqlNameExpression = @"(([\w\d]+\s*::\s*)?((""[^""]+"")|(\[[^\]]+\])|[\w\d]+)\.){0,2}((""[^""]+"")|(\[[^\]]+\])|[\w\d]+)"; + #endregion + #endregion + + #region Formatting Methods + /// + /// Get the name of a SqlObject without owner and schema, and unformat the name + /// + /// [dbo]..[foo] returns foo + /// The full name to clean up + /// The unformatted object name + internal static string UnformatSqlName (string name) + { + string[] splitName = name.Split (_sqlNameDivider); + string objectName = splitName[splitName.Length - 1]; + return _sqlNameCharactersRegex.Replace (objectName, ""); + } + + /// + /// Get the table name from the name of an index + /// + /// The name of the index + /// The name of the table + /// If the table name cannot be determined + internal static string TableNameFromIndexName (string indexName) + { + string[] splitName = indexName.Split (_sqlNameDivider); + string tableName; + switch (splitName.Length) + { + case 3: + tableName = splitName[1]; + break; + case 2: + tableName = splitName[0]; + break; + + default: + throw new ArgumentException (String.Format (CultureInfo.CurrentCulture, Properties.Resources.CannotGetTableNameFromIndexName, indexName)); + } + + return _sqlNameCharactersRegex.Replace (tableName, ""); + } + + /// + /// Format a Sql Name to escape it out properly; + /// + /// The name to escape + /// The escaped name + internal static string FormatSqlName (string name) + { + return String.Format (CultureInfo.InvariantCulture, "[{0}]", UnformatSqlName (name)); + } + + /// + /// Matches characters used to escape a sql name + /// + private static readonly Regex _sqlNameCharactersRegex = new Regex (@"[\[\]\""]"); + + /// + /// The divider between pieces of a sql name + /// + private const char _sqlNameDivider = '.'; + + #endregion + } + #endregion +} diff --git a/Insight.Database.Schema/SchemaObjectCollection.cs b/Insight.Database.Schema/SchemaObjectCollection.cs new file mode 100644 index 0000000..64babd1 --- /dev/null +++ b/Insight.Database.Schema/SchemaObjectCollection.cs @@ -0,0 +1,170 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Reflection; +using System.Collections.ObjectModel; + +#endregion + +namespace Insight.Database.Schema +{ + #region Schema Class + /// + /// Contains a set of SchemaObjects + /// + public class SchemaObjectCollection : Collection + { + #region Constructors + /// + /// Create an empty schema for manual editing + /// + public SchemaObjectCollection () + { + } + + /// + /// Load a schema from a file + /// + /// The name of the file to load + /// If the file canont be found + /// If fileName is null + /// If the schema file cannot be parsed + public SchemaObjectCollection (string fileName) + { + Load (fileName); + } + + /// + /// Load a schema from a stream + /// + /// The stream to load from + public SchemaObjectCollection(Stream stream) + { + Load(stream); + } + + /// + /// Load the schema from the assembly + /// + /// + public SchemaObjectCollection (Assembly assembly) + { + Load (assembly); + } + #endregion + + #region Properties + /// + /// Set to true to strip print statements on load + /// + public bool StripPrintStatements { get; set; } + #endregion + + #region Load Methods + /// + /// Load a schema from a file + /// + /// The name of the file to load + /// If the file canont be found + /// If fileName is null + /// If the schema file cannot be parsed + public void Load (string fileName) + { + if (fileName == null) throw new ArgumentNullException ("fileName"); + + using (Stream stream = new FileStream (fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + Load (stream); + } + + /// + /// Load a schema from a stream + /// + /// The stream to load from + /// If stream is null + /// If the schema file cannot be parsed + public void Load (Stream stream) + { + if (stream == null) throw new ArgumentNullException ("stream"); + + using (TextReader textReader = new StreamReader (stream)) + Load (textReader); + } + + /// + /// Load a schema from a text reader + /// + /// The text reader to load from + /// If textReader is null + /// If the schema file cannot be parsed + public void Load (TextReader textReader) + { + StringBuilder sb = new StringBuilder (); + + // read in each line until we get to a GO command + for (string line = textReader.ReadLine (); line != null; line = textReader.ReadLine ()) + { + if (_goCommandExpression.Match (line).Success) + { + // create the object, add it to the schema, and start a new schema + Add (sb.ToString ()); + sb = new StringBuilder (); + } + else + sb.AppendLine (line); + } + + // if the file doesn't end with a GO, then we need to create one more object + string last = sb.ToString ().Trim(); + if (last.Length > 0) + Add (last); + } + + /// + /// Load a schema from the resource files + /// + /// + public void Load (Assembly assembly) + { + // find all of the embedded sql in the given assembly + foreach (string resourceName in assembly.GetManifestResourceNames ()) + { + if (resourceName.EndsWith (".sql", StringComparison.OrdinalIgnoreCase)) + { + string sql; + + // read in the sql + using (Stream stream = assembly.GetManifestResourceStream (resourceName)) + using (StreamReader reader = new StreamReader (stream)) + sql = reader.ReadToEnd (); + + // now load up the sql + using (StringReader sr = new StringReader (sql)) + Load (sr); + } + } + } + + /// + /// Add a schema object corresponding to a script + /// + /// The sql to add + /// If sql is null + /// If the sql cannot be parsed + public void Add (string sql) + { + // change PRINT to --PRINT so we can keep diagnostics in the + if (StripPrintStatements) + sql = sql.Replace ("PRINT", "--PRINT"); + + Add (new SchemaObject (sql)); + } + #endregion + + private static readonly Regex _goCommandExpression = new Regex (@"^\s*GO\s*$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + } + #endregion +} diff --git a/Insight.Database.Schema/SchemaObjectType.cs b/Insight.Database.Schema/SchemaObjectType.cs new file mode 100644 index 0000000..434c6b7 --- /dev/null +++ b/Insight.Database.Schema/SchemaObjectType.cs @@ -0,0 +1,182 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +#endregion + +namespace Insight.Database.Schema +{ + #region SchemaObjectType Enumeration + /// + /// The type of a database object + /// + /// These are in the order that objects need to be created + public enum SchemaObjectType + { + /// + /// A user script that needs to run first + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "PreScript", Justification="This field value is stored in databases")] + UserPreScript, + + /// + /// A SQL Server 2005 ROLE + /// + Role, + + /// + /// A SQL Server 2005 SCHEMA + /// + Schema, + + /// + /// A user defined type + /// + UserDefinedType, + + /// + /// A database master key + /// + MasterKey, + + /// + /// A certificate + /// + Certificate, + + /// + /// A symmetric key + /// + SymmetricKey, + + /// + /// A partition function + /// + PartitionFunction, + + /// + /// A partition scheme + /// + PartitionScheme, + + /// + /// Service Broker Message Type + /// + MessageType, + + /// + /// Service Broker Contract + /// + Contract, + + /// + /// Service Broker Priority + /// + BrokerPriority, + + /// + /// Service Broker Queue + /// + Queue, + + /// + /// Service Broker Service + /// + Service, + + /// + /// A table in the database + /// + Table, + + /// + /// A primary key on a table + /// + PrimaryKey, + + /// + /// A constraint on a table + /// + Constraint, + + /// + /// A foreign key constraint + /// + ForeignKey, + + /// + /// A user defined function + /// + Function, + + /// + /// An indexed view is marked so that insight won't drop it on a procedure change + /// + IndexedView, + + /// + /// An index + /// + Index, + + /// + /// A view on one or more tables + /// + View, + + /// + /// The primary XML Index on a column + /// + PrimaryXmlIndex, + + /// + /// A secondary XML Index on a column + /// + SecondaryXmlIndex, + + /// + /// A stored procedure + /// + StoredProcedure, + + /// + /// A login to the server + /// + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login")] + Login, + + /// + /// A user in the database + /// + User, + + /// + /// Permission script + /// + Permission, + + /// + /// A table trigger + /// + Trigger, + + /// + /// General padding things added by SQL Server scripter like SET ANSI NULLS ON + /// + Unused, + + /// + /// Objects that were scripted by the dependency generator, or just need to be run by the user + /// + Script, + + /// + /// A script object that just runs when it changes + /// + UserScript, + } + #endregion +} diff --git a/Insight.Database.Schema/SchemaParsingException.cs b/Insight.Database.Schema/SchemaParsingException.cs new file mode 100644 index 0000000..ae13222 --- /dev/null +++ b/Insight.Database.Schema/SchemaParsingException.cs @@ -0,0 +1,79 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; +using System.Globalization; + +#endregion + +namespace Insight.Database.Schema +{ + #region SchemaParsingException Class + /// + /// Represents errors in parsing a SQL script + /// + [Serializable] + public class SchemaParsingException : Exception + { + /// + /// Construct a SchemaParsingException + /// + public SchemaParsingException () + { + } + + /// + /// Construct a SchemaParsingException with a message + /// + /// The exception error message + public SchemaParsingException (string message) : base (message) + { + } + + /// + /// Construct a SchemaParsingException with a message + /// + /// The exception error message + /// The base exception + public SchemaParsingException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Construct a SchemaParsingException with a message and the error sql + /// + /// The exception error message + /// The sql script that could not be parsed + public SchemaParsingException(string message, string sql) + : base(String.Format(CultureInfo.InvariantCulture, message, sql)) + { + _sql = sql; + } + + /// + /// Construct a SchemaParsingException with a message and the error sql + /// + /// Serialization information + /// Serialization context + protected SchemaParsingException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } + + /// + /// The SQL script that caused the error + /// + /// + public string Sql { get { return _sql; } } + private string _sql; + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("_sql", _sql); + } + } + #endregion +} diff --git a/Insight.Database.Schema/SchemaRegistry.cs b/Insight.Database.Schema/SchemaRegistry.cs new file mode 100644 index 0000000..05d22c7 --- /dev/null +++ b/Insight.Database.Schema/SchemaRegistry.cs @@ -0,0 +1,246 @@ +#region Using directives + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Globalization; +using System.Text; + +#endregion + +namespace Insight.Database.Schema +{ + /// + /// Manages the list of schema objects installed in the database. + /// + class SchemaRegistry : IDisposable + { + #region Constructors + /// + /// Manages the signatures of objects in the schema database + /// + /// The connection to the database + public SchemaRegistry (SqlConnection connection) + { + _connection = connection; + CreateTable (); + Load (); + } + #endregion + + #region Public Methods + /// + /// Determine if the database already contains a given object + /// + /// The schema object to look for + /// True if the object is in the registry, false otherwise + public bool Contains (string objectName) + { + string select = String.Format (CultureInfo.InvariantCulture, "ObjectName = '{0}'", objectName); + if (RegistryTable.Select (select).Length > 0) + return true; + else + return false; + } + + /// + /// Delete a schema object from the registry + /// + /// The name of the object + public void DeleteObject (string objectName) + { + DataRow row = FindRow (objectName); + if (row != null) + row.Delete (); + } + + /// + /// Get the signature of an object in the database + /// + /// Name of the object to find + /// The signature of the object + public string GetSignature (string objectName) + { + DataRow row = FindRow (objectName); + return row["Signature"].ToString (); + } + + /// + /// Get the type of an object in the database + /// + /// Name of the object to find + /// The type of the object + public SchemaObjectType GetObjectType (string objectName) + { + DataRow row = FindRow (objectName); + return (SchemaObjectType)Enum.Parse (typeof (SchemaObjectType), row["Type"].ToString ()); + } + + /// + /// Get the original order of an object in the database + /// + /// Name of the object to find + /// The type of the object + public int GetOriginalOrder(string objectName) + { + DataRow row = FindRow(objectName); + object o = row["OriginalOrder"]; + if (o == DBNull.Value) + return 0; + return Convert.ToInt32(o, CultureInfo.InvariantCulture); + } + + /// + /// Add or update an object in the schema registry + /// + /// The object to update + /// The name of the schema group + public void UpdateObject (SchemaObject schemaObject, string schemaGroup) + { + DeleteObject (schemaObject.Name); + RegistryTable.Rows.Add (new object[] { schemaGroup, schemaObject.Name, schemaObject.Signature, schemaObject.SchemaObjectType.ToString (), schemaObject.OriginalOrder }); + } + + /// + /// Returns a list of objects in the given schema group + /// + /// The name of the group to return + /// List of object names + public List GetObjectNames (string schemaGroup) + { + string select = String.Format (CultureInfo.InvariantCulture, "SchemaGroup = '{0}'", schemaGroup); + + // go through the data list and return all of the items in the group + List list = new List (); + foreach (DataRow row in RegistryTable.Select (select)) + { + list.Add (row["ObjectName"].ToString()); + } + + return list; + } + + /// + /// Update the changed registry data + /// + public void Update () + { + _adapter.Update (_registry); + } + #endregion + + #region Private Methods + /// + /// Create the schema registry table in the database + /// + private void CreateTable() + { + // see if the schema registry table already exists + SqlCommand command = new SqlCommand (); + command.Connection = _connection; + command.CommandText = "SELECT COUNT (*) FROM sysobjects WHERE name = @TableName"; + command.Parameters.AddWithValue ("@TableName", _schemaRegistryTableName); + int count = (int)command.ExecuteScalar (); + if (count == 0) + { + // create the schema registry table + command.CommandText = String.Format (CultureInfo.InvariantCulture, @" + CREATE TABLE [{0}] + ( + [SchemaGroup] [varchar](64) NOT NULL, + [ObjectName] [varchar](256) NOT NULL, + [Signature] [varchar](28) NOT NULL, + [Type][varchar](32) NOT NULL, + [OriginalOrder] [int] DEFAULT (0) + CONSTRAINT PK_{0} PRIMARY KEY ([ObjectName]) + ) + ", _schemaRegistryTableName); + } + else + { + // add in the new columns + command.CommandText = String.Format(CultureInfo.InvariantCulture, @" + IF NOT EXISTS (SELECT * FROM sys.syscolumns WHERE id = OBJECT_ID ('insight_schemaregistry') AND name = 'OriginalOrder') + ALTER TABLE {0} ADD OriginalOrder [int] + ", _schemaRegistryTableName); + } + + command.Parameters.Clear (); + command.ExecuteNonQuery (); + } + + /// + /// Load the registry data from the database + /// + private void Load () + { + _adapter = new SqlDataAdapter (String.Format (CultureInfo.InvariantCulture, "SELECT * FROM {0}", _schemaRegistryTableName), _connection); + _registry = new DataSet (); + _registry.Locale = CultureInfo.InvariantCulture; + _adapter.Fill (_registry); + + // use the command builder to fill in the other commands + // this seems to be necessary for some environments + SqlCommandBuilder builder = new SqlCommandBuilder(_adapter); + _adapter.InsertCommand = builder.GetInsertCommand(); + _adapter.UpdateCommand = builder.GetUpdateCommand(); + _adapter.DeleteCommand = builder.GetDeleteCommand(); + + RegistryTable.PrimaryKey = new DataColumn [] { RegistryTable.Columns ["ObjectName"] }; + } + + /// + /// Find a row containing an object + /// + /// The name of the object to find + /// The row containing the object + private DataRow FindRow (string objectName) + { + return RegistryTable.Rows.Find (new object[] { objectName }); + } + #endregion + + #region Private Data + /// + /// The name of the schema registry table + /// + private const string _schemaRegistryTableName = "Insight_SchemaRegistry"; + + /// + /// The connection + /// + private SqlConnection _connection; + + /// + /// The data adapter for synchronizing the registry data + /// + private SqlDataAdapter _adapter; + + /// + /// The data table in memory + /// + /// The DataTable containing the registry records + private DataTable RegistryTable { get { return _registry.Tables[0]; } } + + /// + /// The schema registry + /// + private DataSet _registry; + #endregion + + public void Dispose() + { + if (_registry != null) + { + _registry.Dispose(); + _registry = null; + } + if (_registry != null) + { + _adapter.Dispose(); + _adapter = null; + } + } + } +} diff --git a/Insight.Database.Schema/packages.config b/Insight.Database.Schema/packages.config new file mode 100644 index 0000000..87f1a46 --- /dev/null +++ b/Insight.Database.Schema/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Insight.sln b/Insight.sln new file mode 100644 index 0000000..034b062 --- /dev/null +++ b/Insight.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CEDD5EAD-7CF5-45EE-8676-EFEC0CBAB552}" + ProjectSection(SolutionItems) = preProject + autobuild.bat = autobuild.bat + autobuild.proj = autobuild.proj + Building.txt = Building.txt + License.txt = License.txt + testbuild.bat = testbuild.bat + Version.txt = Version.txt + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Insight.Database.Schema", "Insight.Database.Schema\Insight.Database.Schema.csproj", "{FBD1C65F-9054-42C7-BA0E-48A922C4C747}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FBD1C65F-9054-42C7-BA0E-48A922C4C747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBD1C65F-9054-42C7-BA0E-48A922C4C747}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBD1C65F-9054-42C7-BA0E-48A922C4C747}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBD1C65F-9054-42C7-BA0E-48A922C4C747}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..4808291 --- /dev/null +++ b/License.txt @@ -0,0 +1,56 @@ +Insight Library, a database schema deployment system +Copyright (C) 2005-2012, Jon Wagner (jonwagner@hotmail.com), and others + +=============================================================== +Microsoft Public License (MS-PL) + +This license governs use of the accompanying software. If you use the software, you + accept this license. If you do not accept the license, do not use the software. + +1. Definitions + The terms "reproduce," "reproduction," "derivative works," and "distribution" have the + same meaning here as under U.S. copyright law. + A "contribution" is the original software, or any additions or changes to the software. + A "contributor" is any person that distributes its contribution under this license. + "Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights + (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations + (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. + (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. + (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. + (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. + +=============================================================== + +MSBuild.Community.Tasks - licensed under BSD license. + +http://msbuildtasks.tigris.org/ + +=============================================================== + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=============================================================== + +MSBuild.Mercurial.Tasks - licensed under MIT license. + +http://msbuildhg.codeplex.com/ + +Copyright (c) 2010 François Karman + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================== \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0840554 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.235 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("eMoney Insight")] +[assembly: AssemblyCompany("Insight Module")] +[assembly: AssemblyProduct("Insight")] +[assembly: AssemblyCopyright("Copyright © Jon Wagner, used by permission")] +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] +[assembly: AssemblyVersion("1.0.12.34")] +[assembly: AssemblyFileVersion("1.0.12.34")] + + diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 0000000..210699e --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.1 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Insight.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Insight.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 0000000..a814449 --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs new file mode 100644 index 0000000..44a99a7 --- /dev/null +++ b/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.1 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Insight.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Properties/Settings.settings b/Properties/Settings.settings new file mode 100644 index 0000000..c7fa9fb --- /dev/null +++ b/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Version.txt b/Version.txt new file mode 100644 index 0000000..df42294 --- /dev/null +++ b/Version.txt @@ -0,0 +1 @@ +1.0.54.0 \ No newline at end of file diff --git a/autobuild.bat b/autobuild.bat new file mode 100644 index 0000000..0c7c550 --- /dev/null +++ b/autobuild.bat @@ -0,0 +1 @@ +\windows\Microsoft.NET\Framework\v4.0.30319\msbuild autobuild.proj /t:AutoBuild \ No newline at end of file diff --git a/autobuild.proj b/autobuild.proj new file mode 100644 index 0000000..217b748 --- /dev/null +++ b/autobuild.proj @@ -0,0 +1,87 @@ + + + + + + Release + + + + + + + + + + + + + + + + + + + + + + version.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/localbuild.bat b/localbuild.bat new file mode 100644 index 0000000..d7b9eda --- /dev/null +++ b/localbuild.bat @@ -0,0 +1 @@ +\windows\Microsoft.NET\Framework\v4.0.30319\msbuild autobuild.proj /t:IncrementVersion;Build \ No newline at end of file diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Community.Tasks.Targets b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Community.Tasks.Targets new file mode 100644 index 0000000..95ca5e2 --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Community.Tasks.Targets @@ -0,0 +1,103 @@ + + + + + + MSBuild.Community.Tasks.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Community.Tasks.dll b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Community.Tasks.dll new file mode 100644 index 0000000..cf847a5 Binary files /dev/null and b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Community.Tasks.dll differ diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Mercurial.dll b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Mercurial.dll new file mode 100644 index 0000000..49ffa5b Binary files /dev/null and b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Mercurial.dll differ diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Mercurial.tasks b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Mercurial.tasks new file mode 100644 index 0000000..05774f2 --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Build/MSBuild.Mercurial.tasks @@ -0,0 +1,21 @@ + + + + + MSBuild.Mercurial + MSBuild.Mercurial.dll + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/Init.ps1 b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Init.ps1 new file mode 100644 index 0000000..5734189 --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/Init.ps1 @@ -0,0 +1,28 @@ +param($installPath, $toolsPath, $package, $project) + +$solutionPath = [System.IO.Directory]::GetParent([System.IO.Path]::GetDirectoryName($installPath)).FullName +Write-Host "Installing AutoBuild files to $solutionPath..." + +# always put a new copy of autobuild out there +Copy-Item "$toolsPath\autobuild.proj" "$solutionPath" +$autobuild = [xml](Get-Content("$solutionPath\autobuild.proj")) +$import = $autobuild.CreateElement("Import", "http://schemas.microsoft.com/developer/msbuild/2003") +$import.SetAttribute("Project", "$toolsPath\Build\MSBuild.Community.Tasks.Targets") +$autobuild.Project.AppendChild($import) +$import = $autobuild.CreateElement("Import", "http://schemas.microsoft.com/developer/msbuild/2003") +$import.SetAttribute("Project", "$toolsPath\Build\MSBuild.Mercurial.Tasks") +$autobuild.Project.AppendChild($import) +$autobuild.Save("$solutionPath\autobuild.proj") + +# these files can get modified by the user +if (!(Test-Path("$solutionPath\autobuild.bat"))) { Copy-Item "$toolsPath\autobuild.bat" "$solutionPath" } +if (!(Test-Path("$solutionPath\localbuild.bat"))) { Copy-Item "$toolsPath\localbuild.bat" "$solutionPath" } +if (!(Test-Path("$solutionPath\testbuild.bat"))) { Copy-Item "$toolsPath\testbuild.bat" "$solutionPath" } +if (!(Test-Path("$solutionPath\nuget.exe"))) { Copy-Item "$toolsPath\nuget.exe" "$solutionPath" } +if (!(Test-Path("$solutionPath\version.txt"))) { Copy-Item "$toolsPath\version.txt" "$solutionPath" } + +Write-Host "AutoBuild has been installed." +Write-Host "Next Steps:" +Write-Host "1) Update version.txt for your initial version number" +Write-Host "2) If you want autobuild to publish when complete, add /p:PublishFolder= to autobuild.bat" +Write-Host "" diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/autobuild.bat b/packages/BuildTools.AutoBuild.1.0.17.47/tools/autobuild.bat new file mode 100644 index 0000000..0c7c550 --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/autobuild.bat @@ -0,0 +1 @@ +\windows\Microsoft.NET\Framework\v4.0.30319\msbuild autobuild.proj /t:AutoBuild \ No newline at end of file diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/autobuild.proj b/packages/BuildTools.AutoBuild.1.0.17.47/tools/autobuild.proj new file mode 100644 index 0000000..8723732 --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/autobuild.proj @@ -0,0 +1,132 @@ + + + + + + + Release + + + + + + + + + + + + + + + + + + + + + + + + + + + version.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/localbuild.bat b/packages/BuildTools.AutoBuild.1.0.17.47/tools/localbuild.bat new file mode 100644 index 0000000..d7b9eda --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/localbuild.bat @@ -0,0 +1 @@ +\windows\Microsoft.NET\Framework\v4.0.30319\msbuild autobuild.proj /t:IncrementVersion;Build \ No newline at end of file diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/testbuild.bat b/packages/BuildTools.AutoBuild.1.0.17.47/tools/testbuild.bat new file mode 100644 index 0000000..2102d9c --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/testbuild.bat @@ -0,0 +1 @@ +\windows\Microsoft.NET\Framework\v4.0.30319\msbuild autobuild.proj /t:Build \ No newline at end of file diff --git a/packages/BuildTools.AutoBuild.1.0.17.47/tools/version.txt b/packages/BuildTools.AutoBuild.1.0.17.47/tools/version.txt new file mode 100644 index 0000000..bd2666a --- /dev/null +++ b/packages/BuildTools.AutoBuild.1.0.17.47/tools/version.txt @@ -0,0 +1 @@ +1.0.0.0 \ No newline at end of file diff --git a/packages/BuildTools.FxCop.1.0.17.47/content/FxCop.FxCop b/packages/BuildTools.FxCop.1.0.17.47/content/FxCop.FxCop new file mode 100644 index 0000000..927ab90 --- /dev/null +++ b/packages/BuildTools.FxCop.1.0.17.47/content/FxCop.FxCop @@ -0,0 +1,42 @@ + + + + True + $(FxCopDir)\Xml\FxCopReport.xsl + + + + + + True + True + True + 10 + 1 + + False + + False + 120 + False + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/BuildTools.FxCop.1.0.17.47/tools/FxCop.Targets b/packages/BuildTools.FxCop.1.0.17.47/tools/FxCop.Targets new file mode 100644 index 0000000..43dba8d --- /dev/null +++ b/packages/BuildTools.FxCop.1.0.17.47/tools/FxCop.Targets @@ -0,0 +1,16 @@ + + + true + + + + $(BuildDependsOn);FxCop + + + + + + + + + diff --git a/packages/BuildTools.FxCop.1.0.17.47/tools/Install.ps1 b/packages/BuildTools.FxCop.1.0.17.47/tools/Install.ps1 new file mode 100644 index 0000000..106be70 --- /dev/null +++ b/packages/BuildTools.FxCop.1.0.17.47/tools/Install.ps1 @@ -0,0 +1,38 @@ +param($installPath, $toolsPath, $package, $project) + +# if there is a project file, then modify it +if ($project) +{ + # *************************************************** + # Modify the build project to import the stylecop.targets file + # *************************************************** + # This is the MSBuild targets file to add + Write-Host "Adding FxCop.targets" + $targetsFile = [System.IO.Path]::Combine($toolsPath, 'FxCop.targets') + + # Need to load MSBuild assembly if it's not loaded yet. + Add-Type -AssemblyName 'Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' + # Grab the loaded MSBuild project for the project + $msbuild = [Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection.GetLoadedProjects($project.FullName) | Select-Object -First 1 + + # Make the path to the targets file relative. + $projectUri = new-object Uri('file://' + $project.FullName) + $targetUri = new-object Uri('file://' + $targetsFile) + $relativePath = $projectUri.MakeRelativeUri($targetUri).ToString().Replace([System.IO.Path]::AltDirectorySeparatorChar, [System.IO.Path]::DirectorySeparatorChar) + + # Add the import and save the project + $msbuild.Xml.AddImport($relativePath) | out-null + $project.Save() + + # *************************************************** + # Modify the per-project fxcop settings file to point at the project dll + # *************************************************** + Write-Host "Adding default target to FxCop.FxCop file" + $fxCopFile = [System.IO.Path]::GetDirectoryName($project.filename) + "\FxCop.FxCop" + $projectUri = new-object Uri('file://' + $project.FullName) + $targetUri = new-object Uri('file://' + $fxCopFile) + $relativePath = $projectUri.MakeRelativeUri($targetUri).ToString().Replace([System.IO.Path]::AltDirectorySeparatorChar, [System.IO.Path]::DirectorySeparatorChar) + $settings = [xml](get-content($fxCopFile)) + $settings.SelectSingleNode("//Target").SetAttribute("Name", "`$(ProjectDir)/bin/Release/" + $project.name + ".dll") + $settings.Save($fxCopFile) +} diff --git a/packages/BuildTools.FxCop.1.0.17.47/tools/Uninstall.ps1 b/packages/BuildTools.FxCop.1.0.17.47/tools/Uninstall.ps1 new file mode 100644 index 0000000..e7b95af --- /dev/null +++ b/packages/BuildTools.FxCop.1.0.17.47/tools/Uninstall.ps1 @@ -0,0 +1,27 @@ +param($installPath, $toolsPath, $package, $project) + +# this is the name of the package folder +$packageFolder = $package.ID + "." + $package.Version + +# if there is a project file, then modify it +if ($project) +{ + Write-Host "Removing FxCop.targets from project" + + # This is the MSBuild targets file to add + $targetsFile = [System.IO.Path]::Combine($toolsPath, 'FxCop.targets') + + # Need to load MSBuild assembly if it's not loaded yet. + Add-Type -AssemblyName 'Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' + # Grab the loaded MSBuild project for the project + $msbuild = [Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection.GetLoadedProjects($project.FullName) | Select-Object -First 1 + + # Make the path to the targets file relative. + $projectUri = new-object Uri('file://' + $project.FullName) + $targetUri = new-object Uri('file://' + $targetsFile) + $relativePath = $projectUri.MakeRelativeUri($targetUri).ToString().Replace([System.IO.Path]::AltDirectorySeparatorChar, [System.IO.Path]::DirectorySeparatorChar) + + # Remove the import and save the project + $msbuild.Xml.Imports | Where { $_.Project -eq $relativePath } | % { $msbuild.Xml.RemoveChild($_) } + $project.Save() +} \ No newline at end of file diff --git a/packages/repositories.config b/packages/repositories.config new file mode 100644 index 0000000..f0bb134 --- /dev/null +++ b/packages/repositories.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/testbuild.bat b/testbuild.bat new file mode 100644 index 0000000..2102d9c --- /dev/null +++ b/testbuild.bat @@ -0,0 +1 @@ +\windows\Microsoft.NET\Framework\v4.0.30319\msbuild autobuild.proj /t:Build \ No newline at end of file