diff --git a/GlobalAssemblyInfo.cs b/GlobalAssemblyInfo.cs index 0f006d3..6e24e96 100644 --- a/GlobalAssemblyInfo.cs +++ b/GlobalAssemblyInfo.cs @@ -7,6 +7,6 @@ [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en-US")] -[assembly: AssemblyVersion("1.0.3.5")] -[assembly: AssemblyFileVersion("1.0.3.5")] -[assembly: AssemblyInformationalVersion("1.0.3.5")] \ No newline at end of file +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] +[assembly: AssemblyInformationalVersion("2.0.0.0")] \ No newline at end of file diff --git a/Improving.DbUp.sln b/Improving.DbUp.sln index 1e9f544..fe4a770 100644 --- a/Improving.DbUp.sln +++ b/Improving.DbUp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28407.52 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{6A5A7337-11EB-4B20-8B60-9FBF234019D4}" ProjectSection(SolutionItems) = preProject @@ -37,4 +37,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {33A587EE-C169-4045-AC85-C3C41449780B} + EndGlobalSection EndGlobal diff --git a/Source/Improving.DbUp.QuickStart/Improving.DbUp.QuickStart.nuspec b/Source/Improving.DbUp.QuickStart/Improving.DbUp.QuickStart.nuspec index 103ee02..de32cc6 100644 --- a/Source/Improving.DbUp.QuickStart/Improving.DbUp.QuickStart.nuspec +++ b/Source/Improving.DbUp.QuickStart/Improving.DbUp.QuickStart.nuspec @@ -1,10 +1,10 @@  - Improving.DbUp.QuickStart + Improving.DbUp.FileSystem.QuickStart $version$ $title$ - Craig Neuwirt,Michael Dudley,Cori Drew + Craig Neuwirt,Michael Dudley,Cori Drew,Rumit Parakhiya Improving false $description$ diff --git a/Source/Improving.DbUp.QuickStart/readme.txt b/Source/Improving.DbUp.QuickStart/readme.txt index b377c8e..2098001 100644 --- a/Source/Improving.DbUp.QuickStart/readme.txt +++ b/Source/Improving.DbUp.QuickStart/readme.txt @@ -26,7 +26,9 @@ internal class Program var shouldSeedData = env == Env.LOCAL; var dbName = ConfigurationManager.AppSettings["DbName"]; - var dbUpdater = new DbUpdater(Assembly.GetExecutingAssembly(), "Scripts", dbName, connectionStringName, scriptVariables, shouldSeedData, env); + var dbUpdater = DbUpdater.UsingEmbeddedScripts(Assembly.GetExecutingAssembly(), "Scripts", dbName, connectionStringName, scriptVariables, shouldSeedData, env); + // Or + // var dbUpdater = DbUpdater.UsingScriptsInFileSystem(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Scripts"), dbName, connectionStringName, scriptVariables, null, shouldSeedData, env) return dbUpdater.Run() ? 0 : -1; } } diff --git a/Source/Improving.DbUp/DbUpdater.cs b/Source/Improving.DbUp/DbUpdater.cs index 7f9e8e4..1250899 100644 --- a/Source/Improving.DbUp/DbUpdater.cs +++ b/Source/Improving.DbUp/DbUpdater.cs @@ -3,86 +3,94 @@ using System.Configuration; using System.Data; using System.Data.SqlClient; +using System.IO; +using System.Linq; using System.Reflection; using DbUp; using DbUp.Builder; +using DbUp.Engine; +using DbUp.Engine.Output; using DbUp.Helpers; +using DbUp.ScriptProviders; +using DbUp.SqlServer; +using Improving.DbUp.Hashed; namespace Improving.DbUp { - using global::DbUp.Engine.Output; - using global::DbUp.Support.SqlServer; - using Hashed; - - public class DbUpdater + public abstract class DbUpdater { - private readonly string _namespacePrefix; - private readonly Assembly _migrationAssembly; private readonly bool _seedData; private readonly Env _env; + private readonly IUpgradeLog _upgradeLog; + + public virtual string FolderName { get; } + public virtual string DatabaseName { get; } + public virtual string ConnectionString { get; } + public virtual bool UseTransactions { get; set; } + public virtual IDictionary ScriptVariables { get; } - public DbUpdater(Assembly migrationAssembly, + protected abstract string RootPrefix { get; } + protected abstract string PathSeparator { get; } + + public DbUpdater( string folderName, string databaseName, string connectionStringName, IDictionary scriptVariables, - bool SeedData = false, - Env env = Env.Undefined) + bool seedData = false, + Env env = Env.Undefined, + IUpgradeLog upgradeLog = null) { - _namespacePrefix = migrationAssembly.GetName().Name + "."; - _migrationAssembly = migrationAssembly; - _seedData = SeedData; - _env = env; + _seedData = seedData; + _env = env; + + _upgradeLog = upgradeLog; + if (_upgradeLog == null) + { + _upgradeLog = new ConsoleUpgradeLog(); + } ConnectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString + ";App=" + databaseName + "Migrations"; - UseTransactions = true; - DatabaseName = databaseName; - FolderName = folderName; - ScriptVariables = scriptVariables; + UseTransactions = true; + DatabaseName = databaseName; + FolderName = folderName; + ScriptVariables = scriptVariables; } - public string FolderName { get; } - public string DatabaseName { get; } - public string ConnectionString { get; } - public bool UseTransactions { get; set; } - public IDictionary ScriptVariables { get; } - public bool Run() { try { if (IsNew()) { - Console.WriteLine("No '{0}' database found. One will be created.", DatabaseName); + _upgradeLog.WriteInformation("No '{0}' database found. One will be created.", DatabaseName); CreateDatabase(); } - Console.WriteLine("Performing beforeMigrations for '{0}' database.", DatabaseName); + _upgradeLog.WriteInformation("Performing beforeMigrations for '{0}' database.", DatabaseName); BeforeMigration(); - Console.WriteLine("Performing migrations for '{0}' database.", DatabaseName); + _upgradeLog.WriteInformation("Performing migrations for '{0}' database.", DatabaseName); MigrateDatabase(); - Console.WriteLine("Executing hashed scripts for '{0}' database.", DatabaseName); + _upgradeLog.WriteInformation("Executing hashed scripts for '{0}' database.", DatabaseName); HashedScripts(); - Console.WriteLine("Executing always run scripts for '{0}' database.", DatabaseName); + _upgradeLog.WriteInformation("Executing always run scripts for '{0}' database.", DatabaseName); AlwaysRun(); - Console.WriteLine("Executing test scripts for '{0}' database. These are run every time.", DatabaseName); + _upgradeLog.WriteInformation("Executing test scripts for '{0}' database. These are run every time.", DatabaseName); UpdateTests(); if (_seedData) { - Console.WriteLine("Executing seed scripts for '{0}' database.", DatabaseName); + _upgradeLog.WriteInformation("Executing seed scripts for '{0}' database.", DatabaseName); SeedDatabase(); } } catch (Exception e) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(e.Message); - Console.ResetColor(); + _upgradeLog.WriteError(e.Message); return false; } @@ -93,7 +101,7 @@ public bool Run() private void HashedScripts() { ExecuteHashedDatabaseActions( - _namespacePrefix + FolderName + ".Hash", + JoinPath(RootPrefix, FolderName, "Hash"), ConnectionString, builder => UseTransactions ? builder.WithTransactionPerScript() : builder); } @@ -101,7 +109,7 @@ private void HashedScripts() private void AlwaysRun() { ExecuteDatabaseActions( - _namespacePrefix + FolderName + ".AlwaysRun", + JoinPath(RootPrefix, FolderName, "AlwaysRun"), ConnectionString, builder => builder.JournalTo(new NullJournal())); } @@ -109,7 +117,7 @@ private void AlwaysRun() private void BeforeMigration() { ExecuteDatabaseActions( - _namespacePrefix + FolderName + ".BeforeMigration", + JoinPath(RootPrefix, FolderName, "BeforeMigration"), ConnectionString, builder => UseTransactions ? builder.WithTransactionPerScript() : builder); } @@ -117,7 +125,7 @@ private void BeforeMigration() private void MigrateDatabase() { ExecuteDatabaseActions( - _namespacePrefix + FolderName + ".Migration", + JoinPath(RootPrefix, FolderName, "Migration"), ConnectionString, builder => UseTransactions ? builder.WithTransactionPerScript() : builder); } @@ -126,11 +134,11 @@ private void UpdateTests() { if (_env != Env.LOCAL && _env != Env.DEV && _env != Env.QA) { - Console.WriteLine("Scripts in the test folder are only executed in LOCAL, DEV and QA"); + _upgradeLog.WriteInformation("Scripts in the test folder are only executed in LOCAL, DEV and QA"); return; } ExecuteDatabaseActions( - _namespacePrefix + FolderName + ".Test", + JoinPath(RootPrefix, FolderName, "Test"), ConnectionString, builder => builder.JournalTo(new NullJournal())); } @@ -138,7 +146,7 @@ private void UpdateTests() private void SeedDatabase() { ExecuteDatabaseActions( - _namespacePrefix + FolderName + ".Seed", + JoinPath(RootPrefix, FolderName, "Seed"), ConnectionString, builder => UseTransactions ? builder.WithTransactionPerScript() : builder); } @@ -151,7 +159,7 @@ private void CreateDatabase() }; ExecuteDatabaseActions( - _namespacePrefix + FolderName + ".FirstRun", + JoinPath(RootPrefix, FolderName, "FirstRun"), masterDbSqlConnection.ConnectionString, builder => builder.JournalTo(new NullJournal())); } @@ -163,14 +171,15 @@ private void ExecuteDatabaseActions(string scriptPrefix, string connectionString DeployChanges.To .SqlDatabase(connectionString) .WithExecutionTimeout(TimeSpan.FromSeconds(2147483647)) - .WithScriptsEmbeddedInAssembly(_migrationAssembly, - name => name.StartsWith(scriptPrefix)) + .WithScripts(UnderlyingScriptProvider(scriptPrefix)) .WithVariables(ScriptVariables) .LogToConsole(); - + PerformUpgrade(customBuilderTransform, builder); } + protected abstract IScriptProvider UnderlyingScriptProvider(string scriptPrefix); + private void PerformUpgrade(Func customBuilderTransform, UpgradeEngineBuilder builder) { var upgrader = customBuilderTransform(builder).Build(); @@ -178,15 +187,12 @@ private void PerformUpgrade(Func cus if (!createResult.Successful) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(createResult.Error); + _upgradeLog.WriteError(createResult.Error.Message); throw createResult.Error; } else { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Success!"); - Console.ResetColor(); + _upgradeLog.WriteInformation("Success!"); } } @@ -194,14 +200,13 @@ private void ExecuteHashedDatabaseActions(string scriptPrefix, string connection Func customBuilderTransform) { var sqlConnectionManager = new SqlConnectionManager(connectionString); - var log = new ConsoleUpgradeLog(); - var journal = new HashedSqlTableJournal(() => sqlConnectionManager, () => log, null, HashedSqlServerExtensions.VersionTableName); + var journal = new HashedSqlTableJournal(() => sqlConnectionManager, () => _upgradeLog, null, HashedSqlServerExtensions.VersionTableName); var builder = DeployChanges.To .HashedSqlDatabase(sqlConnectionManager) .WithExecutionTimeout(TimeSpan.FromSeconds(2147483647)) - .WithHashedScriptsEmbeddedInAssembly(_migrationAssembly, name => name.StartsWith(scriptPrefix), journal) + .WithHashedScripts(UnderlyingScriptProvider(scriptPrefix), journal) .WithVariables(ScriptVariables) .LogToConsole(); @@ -230,5 +235,45 @@ private bool IsNew() } } } + + private string JoinPath(params string[] values) + { + return string.Join(PathSeparator, values.Where(p => !String.IsNullOrWhiteSpace(p?.Trim()))); + } + + #region Factory Methods + public static DbUpdater UsingEmbeddedScripts(Assembly migrationAssembly, + string folderName, + string databaseName, + string connectionStringName, + IDictionary scriptVariables, + bool seedData = false, + Env env = Env.Undefined, + IUpgradeLog logger = null) + { + return new EmbeddedScriptsDbUpdater(migrationAssembly, folderName, databaseName, connectionStringName, scriptVariables, seedData, env, logger); + } + + public static DbUpdater UsingScriptsInFileSystem(string folderName, + string databaseName, + string connectionStringName, + IDictionary scriptVariables, + FileSystemScriptOptions fileSystemScriptOptions = null, + bool seedData = false, + Env env = Env.Undefined, + IUpgradeLog logger = null) + { + if (fileSystemScriptOptions == null) + { + fileSystemScriptOptions = new FileSystemScriptOptions() + { + IncludeSubDirectories = true, + Filter = name => name.EndsWith(".sql") + }; + } + + return new FileSystemScriptsDbUpdater(folderName, fileSystemScriptOptions, databaseName, connectionStringName, scriptVariables, seedData, env, logger); + } + #endregion } } diff --git a/Source/Improving.DbUp/EmbeddedScriptsDbUpdater.cs b/Source/Improving.DbUp/EmbeddedScriptsDbUpdater.cs new file mode 100644 index 0000000..4fafde1 --- /dev/null +++ b/Source/Improving.DbUp/EmbeddedScriptsDbUpdater.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using DbUp.Builder; +using DbUp.Engine; +using DbUp.Engine.Output; +using DbUp.ScriptProviders; + +namespace Improving.DbUp +{ + public class EmbeddedScriptsDbUpdater : DbUpdater + { + private readonly string _namespacePrefix; + private readonly Assembly _migrationAssembly; + + public EmbeddedScriptsDbUpdater(Assembly migrationAssembly, + string folderName, + string databaseName, + string connectionStringName, + IDictionary scriptVariables, + bool seedData = false, + Env env = Env.Undefined, + IUpgradeLog logger = null) + : base(folderName, databaseName, connectionStringName, scriptVariables, seedData, env, logger) + { + _namespacePrefix = migrationAssembly.GetName().Name; + _migrationAssembly = migrationAssembly; + } + + protected override string RootPrefix => _namespacePrefix; + + protected override string PathSeparator => "."; + + protected override IScriptProvider UnderlyingScriptProvider(string scriptPrefix) + { + return new EmbeddedScriptProvider(_migrationAssembly, name => name.StartsWith(scriptPrefix)); + } + } +} diff --git a/Source/Improving.DbUp/FileSystemScriptsDbUpdater.cs b/Source/Improving.DbUp/FileSystemScriptsDbUpdater.cs new file mode 100644 index 0000000..fb4dc3d --- /dev/null +++ b/Source/Improving.DbUp/FileSystemScriptsDbUpdater.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DbUp.Engine; +using DbUp.Engine.Output; +using DbUp.ScriptProviders; + +namespace Improving.DbUp +{ + public class FileSystemScriptsDbUpdater : DbUpdater + { + private readonly FileSystemScriptOptions _fileSystemScriptOptions; + + public FileSystemScriptsDbUpdater(string folderName, + FileSystemScriptOptions fileSystemScriptOptions, + string databaseName, + string connectionStringName, + IDictionary scriptVariables, + bool seedData = false, + Env env = Env.Undefined, + IUpgradeLog upgradeLog = null) + : base(folderName, databaseName, connectionStringName, scriptVariables, seedData, env, upgradeLog) + { + this._fileSystemScriptOptions = fileSystemScriptOptions; + } + + protected override string RootPrefix => String.Empty; + + protected override string PathSeparator => Path.DirectorySeparatorChar.ToString(); + + protected override IScriptProvider UnderlyingScriptProvider(string scriptPrefix) + { + return new FileSystemScriptProvider(scriptPrefix, _fileSystemScriptOptions); + } + } +} diff --git a/Source/Improving.DbUp/Hashed/HashedEmbeddedScriptsProvider.cs b/Source/Improving.DbUp/Hashed/HashedEmbeddedScriptsProvider.cs index b18a30c..97c7c12 100644 --- a/Source/Improving.DbUp/Hashed/HashedEmbeddedScriptsProvider.cs +++ b/Source/Improving.DbUp/Hashed/HashedEmbeddedScriptsProvider.cs @@ -8,12 +8,11 @@ using global::DbUp.Engine; using global::DbUp.Engine.Transactions; - public class HashedEmbeddedScriptsProvider : IScriptProvider + public class HashedEmbeddedScriptsProvider : HashedScriptsProvider, IScriptProvider { private readonly Assembly _assembly; private readonly Encoding _encoding; private readonly Func _filter; - private readonly HashedSqlTableJournal _journal; /// /// Initializes a new instance of the class. @@ -21,30 +20,18 @@ public class HashedEmbeddedScriptsProvider : IScriptProvider /// The assemblies to search. /// The filter. /// The encoding. - /// The Journal public HashedEmbeddedScriptsProvider(Assembly assembly, Func filter, Encoding encoding, - IJournal journal) + IHashedJournal journal) + : base(journal) { _assembly = assembly; _filter = filter; _encoding = encoding; - _journal = (HashedSqlTableJournal)journal; } - /// - /// Gets all scripts that should be executed. - /// - /// - public IEnumerable GetScripts(IConnectionManager connectionManager) + protected override IEnumerable GetAllScripts() { - var executedScriptInfo = _journal.GetExecutedScriptDictionary(); - var allScripts = GetAssemblyScripts(); - - return allScripts - .Where(script => - !executedScriptInfo.ContainsKey(script.Name) - || (executedScriptInfo.ContainsKey(script.Name) && executedScriptInfo[script.Name] != Md5Utils.Md5EncodeString(script.Contents))) - .ToList(); + return this.GetAssemblyScripts(); } private IEnumerable GetAssemblyScripts() diff --git a/Source/Improving.DbUp/Hashed/HashedFileSystemScriptsProvider.cs b/Source/Improving.DbUp/Hashed/HashedFileSystemScriptsProvider.cs new file mode 100644 index 0000000..e743738 --- /dev/null +++ b/Source/Improving.DbUp/Hashed/HashedFileSystemScriptsProvider.cs @@ -0,0 +1,64 @@ +using DbUp.Engine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DbUp.ScriptProviders; +using System.IO; + +namespace Improving.DbUp.Hashed +{ + public class HashedFileSystemScriptsProvider : HashedScriptsProvider, IScriptProvider + { + private readonly string _directoryPath; + private readonly FileSystemScriptOptions _fileSystemScriptOptions; + private readonly Func _filter; + + public HashedFileSystemScriptsProvider(string directoryPath, FileSystemScriptOptions fileSystemScriptOptions, Func filter, IHashedJournal journal) + : base(journal) + { + _directoryPath = directoryPath; + _fileSystemScriptOptions = fileSystemScriptOptions; + this._filter = filter; + } + + public override char PathSeparator => Path.PathSeparator; + + protected override IEnumerable GetAllScripts() + { + var files = GetAllFiles(_directoryPath, _fileSystemScriptOptions) + .Select(file => file.FullName) + .Where(_filter) + .Select(file => SqlScript.FromFile(file)) + .OrderBy(script => script.Name) + .ToList(); + + return files; + } + + private IEnumerable GetAllFiles(string directoryPath, FileSystemScriptOptions fileSystemScriptOptions) + { + var files = new List(); + + if (!Directory.Exists(directoryPath)) + { + throw new DirectoryNotFoundException($"No directory found at {directoryPath}."); + } + + var directory = new DirectoryInfo(directoryPath); + + if (fileSystemScriptOptions.IncludeSubDirectories) + { + foreach (var subDirectory in directory.EnumerateDirectories()) + { + files.AddRange(GetAllFiles(subDirectory.FullName, fileSystemScriptOptions)); + } + } + + files.AddRange(directory.GetFiles()); + + return files; + } + } +} diff --git a/Source/Improving.DbUp/Hashed/HashedScriptsProvider.cs b/Source/Improving.DbUp/Hashed/HashedScriptsProvider.cs new file mode 100644 index 0000000..63b8c4b --- /dev/null +++ b/Source/Improving.DbUp/Hashed/HashedScriptsProvider.cs @@ -0,0 +1,61 @@ +using DbUp.Engine; +using DbUp.Engine.Transactions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Improving.DbUp.Hashed +{ + public class HashedScriptsProvider : IScriptProvider + { + private readonly IHashedJournal _journal; + private readonly IScriptProvider _underlyingScriptProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The Journal + public HashedScriptsProvider(IHashedJournal journal) + { + _journal = journal; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Journal + /// Native script provider. EmbeddedScriptsProvider, FileSystemScriptsProvider, etc + public HashedScriptsProvider(IHashedJournal journal, IScriptProvider underlyingScriptsProvider) + { + _journal = journal; + this._underlyingScriptProvider = underlyingScriptsProvider; + } + + /// + /// Gets all scripts. filters already executeds scripts from the list returned by this method. + /// + /// Sql scripts + protected virtual IEnumerable GetAllScripts(IConnectionManager connectionManager) + { + return this._underlyingScriptProvider.GetScripts(connectionManager); + } + + /// + /// Gets all scripts that should be executed. + /// + /// + public virtual IEnumerable GetScripts(IConnectionManager connectionManager) + { + var executedScriptInfo = _journal.GetExecutedScriptDictionary(); + var allScripts = this.GetAllScripts(connectionManager); + + return allScripts + .Where(script => + !executedScriptInfo.ContainsKey(script.Name) + || (executedScriptInfo.ContainsKey(script.Name) && executedScriptInfo[script.Name] != Md5Utils.Md5EncodeString(script.Contents))) + .ToList(); + } + } +} diff --git a/Source/Improving.DbUp/Hashed/HashedSqlServerExtensions.cs b/Source/Improving.DbUp/Hashed/HashedSqlServerExtensions.cs index a11ed2a..12c5fa3 100644 --- a/Source/Improving.DbUp/Hashed/HashedSqlServerExtensions.cs +++ b/Source/Improving.DbUp/Hashed/HashedSqlServerExtensions.cs @@ -3,36 +3,31 @@ using System; using System.Reflection; using System.Text; + using global::DbUp; using global::DbUp.Builder; using global::DbUp.Engine; - using global::DbUp.Support.SqlServer; - + using global::DbUp.ScriptProviders; + using global::DbUp.SqlServer; + public static class HashedSqlServerExtensions { public const string VersionTableName = "SchemaVersions"; + private static Func JournalFactory = (UpgradeConfiguration upgradeConfiguration) => new HashedSqlTableJournal(() => upgradeConfiguration.ConnectionManager, () => upgradeConfiguration.Log, null, VersionTableName); + public static UpgradeEngineBuilder HashedSqlDatabase(this SupportedDatabases supported, SqlConnectionManager connectionManager) { var builder = new UpgradeEngineBuilder(); builder.Configure(c => c.ConnectionManager = connectionManager); - builder.Configure(c => c.ScriptExecutor = new SqlScriptExecutor(() => c.ConnectionManager, () => c.Log, null, () => c.VariablesEnabled, c.ScriptPreprocessors)); - builder.Configure(c => c.Journal = new HashedSqlTableJournal(() => c.ConnectionManager, () => c.Log, null, VersionTableName)); + builder.Configure(c => c.ScriptExecutor = new SqlScriptExecutor(() => c.ConnectionManager, () => c.Log, null, () => c.VariablesEnabled, c.ScriptPreprocessors, () => JournalFactory(c))); + builder.Configure(c => c.Journal = JournalFactory(c)); return builder; } - - /// - /// Adds all scripts found as embedded resources in the given assembly. - /// - /// The builder. - /// The assembly. - /// The filter. - /// The journal. - /// - /// The same builder - /// - public static UpgradeEngineBuilder WithHashedScriptsEmbeddedInAssembly(this UpgradeEngineBuilder builder, Assembly assembly, Func filter, IJournal journal) + + public static UpgradeEngineBuilder WithHashedScripts(this UpgradeEngineBuilder builder, IScriptProvider scriptProvider, IHashedJournal journal) { - return WithScripts(builder, new HashedEmbeddedScriptsProvider(assembly, filter, Encoding.Default, journal)); + var hashedScriptsProvider = new HashedScriptsProvider(journal, scriptProvider); + return WithScripts(builder, hashedScriptsProvider); } /// diff --git a/Source/Improving.DbUp/Hashed/HashedSqlTableJournal.cs b/Source/Improving.DbUp/Hashed/HashedSqlTableJournal.cs index e0f65be..6d9c1a0 100644 --- a/Source/Improving.DbUp/Hashed/HashedSqlTableJournal.cs +++ b/Source/Improving.DbUp/Hashed/HashedSqlTableJournal.cs @@ -1,135 +1,113 @@ -namespace Improving.DbUp.Hashed +using DbUp.Engine; +using DbUp.Engine.Output; +using DbUp.Engine.Transactions; +using DbUp.SqlServer; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Improving.DbUp.Hashed { - using System; - using System.Collections.Generic; - using System.Data; - using System.Data.Common; - using System.Data.SqlClient; - using global::DbUp.Engine; - using global::DbUp.Engine.Output; - using global::DbUp.Engine.Transactions; - using global::DbUp.Support.SqlServer; - - public class HashedSqlTableJournal : IJournal + public class HashedSqlTableJournal : SqlTableJournal, IHashedJournal { - private readonly Func _connectionManager; - private readonly Func _log; - private readonly string _schema; - private readonly string _table; + private const string ScriptHashParameter = "@scriptHash"; - /// - /// Initializes a new instance of the class. - /// - /// The connection manager. - /// The log. - /// The schema that contains the table. - /// The table name. - public HashedSqlTableJournal( - Func connectionManager, - Func logger, - string schema, - string table) - { - _schema = schema; - _table = table; + private readonly ISqlObjectParser _sqlObjectParser; - _connectionManager = connectionManager; + public HashedSqlTableJournal(Func connectionManager, Func logger, string schema, string table) + : base(connectionManager, logger, schema, table) + { + this._sqlObjectParser = new SqlServerObjectParser(); + } - _log = logger; + protected override string GetInsertJournalEntrySql(string scriptName, string applied) + { + return this.GetInsertJournalEntrySql(scriptName, applied, ScriptHashParameter); } - /// - /// Recalls the version number of the database. - /// - /// All executed scripts. - public string[] GetExecutedScripts() + private string GetInsertJournalEntrySql(string @scriptName, string @applied, string @scriptHash) { - // note: the HashedEmbeddedScriptsProvider implementation will deal with "already/should" run determination so we don't want any "executed scripts" in the pipeline - return new string[0]; + + return $@"MERGE {FqSchemaTableName} AS [Target] + USING(SELECT {@scriptName} as scriptName, {@applied} as applied, {@scriptHash} as scriptHash) AS [Source] + ON [Target].scriptName = [Source].scriptName + WHEN MATCHED THEN + UPDATE SET[Target].applied = [Source].applied, [Target].scriptHash = [Source].scriptHash + WHEN NOT MATCHED THEN + INSERT(ScriptName, Applied, ScriptHash) VALUES([Source].scriptName, [Source].applied, [Source].scriptHash); "; } - /// - /// Records a database upgrade for a database specified in a given connection string. - /// - /// The script. - public void StoreExecutedScript(SqlScript script) + public override void StoreExecutedScript(SqlScript script, Func dbCommandFactory) { - var exists = DoesTableExist(); - if (!exists) + EnsureTableExistsAndIsLatestVersion(dbCommandFactory); + using (var command = this.GetInsertScriptCommand(dbCommandFactory, script)) { - _log().WriteInformation($"Creating the {CreateTableName(_schema, _table)} table"); + command.ExecuteNonQuery(); + } + } - _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => - { - using (var command = dbCommandFactory()) - { - command.CommandText = CreateTableSql(_schema, _table); + private new IDbCommand GetInsertScriptCommand(Func dbCommandFactory, SqlScript script) + { + var command = base.GetInsertScriptCommand(dbCommandFactory, script); - command.CommandType = CommandType.Text; - command.ExecuteNonQuery(); - } + var scriptHashParameter = command.CreateParameter(); + scriptHashParameter.ParameterName = "scriptHash"; + scriptHashParameter.Value = Md5Utils.Md5EncodeString(script.Contents); + command.Parameters.Add(scriptHashParameter); - _log() - .WriteInformation($"The {CreateTableName(_schema, _table)} table has been created"); - }); - } - EnsureHashColumnExists(); + return command; + } - _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => - { - using (var command = dbCommandFactory()) - { - command.CommandText = - $@" - MERGE {CreateTableName(_schema, _table)} AS [Target] - USING(SELECT @scriptName as scriptName, @applied as applied, @scriptHash as scriptHash) AS [Source] - ON [Target].scriptName = [Source].scriptName - WHEN MATCHED THEN - UPDATE SET[Target].applied = [Source].applied, [Target].scriptHash = [Source].scriptHash - WHEN NOT MATCHED THEN - INSERT(ScriptName, Applied, ScriptHash) VALUES([Source].scriptName, [Source].applied, [Source].scriptHash); "; - - // todo: strip off the |HASH" from Name: - var scriptNameParam = command.CreateParameter(); - scriptNameParam.ParameterName = "scriptName"; - scriptNameParam.Value = script.Name; - command.Parameters.Add(scriptNameParam); - - var appliedParam = command.CreateParameter(); - appliedParam.ParameterName = "applied"; - appliedParam.Value = DateTime.Now; - command.Parameters.Add(appliedParam); - - var hashParam = command.CreateParameter(); - hashParam.ParameterName = "scriptHash"; - hashParam.Value = Md5Utils.Md5EncodeString(script.Contents); - command.Parameters.Add(hashParam); + protected override string GetJournalEntriesSql() + { + // note: the HashedEmbeddedScriptsProvider implementation will deal with "already/should" run determination so we don't want any "executed scripts" in the pipeline + return $"SELECT * FROM {FqSchemaTableName} WHERE 1 = 2"; + } - command.CommandType = CommandType.Text; - command.ExecuteNonQuery(); - } - }); + protected override string CreateSchemaTableSql(string quotedPrimaryKeyName) + { + return $@"create table {FqSchemaTableName} ( + [Id] int identity(1,1) not null constraint {quotedPrimaryKeyName} primary key, + [ScriptName] nvarchar(255) not null, + [Applied] datetime not null, + [ScriptHash] nvarchar(255) null + )"; } - public Dictionary GetExecutedScriptDictionary() + public override void EnsureTableExistsAndIsLatestVersion(Func dbCommandFactory) { - _log().WriteInformation("Fetching list of already executed scripts with their known hash."); - var exists = DoesTableExist(); - if (!exists) + base.EnsureTableExistsAndIsLatestVersion(dbCommandFactory); + + var createHashColumnSql = $"if not exists (select column_name from INFORMATION_SCHEMA.columns where table_name = '{UnquotedSchemaTableName}' " + + $" and column_name = 'ScriptHash')" + + $" alter table {FqSchemaTableName} add ScriptHash nvarchar(255)"; + + using (var command = dbCommandFactory()) { - _log() - .WriteInformation( - $"The {CreateTableName(_schema, _table)} table could not be found. The database is assumed to be at version 0."); - return new Dictionary(); + command.CommandText = createHashColumnSql; + command.CommandType = CommandType.Text; + + command.ExecuteNonQuery(); } - EnsureHashColumnExists(); + } + + + // ---------------------- + + public Dictionary GetExecutedScriptDictionary() + { + Log().WriteInformation("Fetching list of already executed scripts with their known hash."); + ConnectionManager().ExecuteCommandsWithManagedConnection(this.EnsureTableExistsAndIsLatestVersion); var scripts = new Dictionary(); - _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => + ConnectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => { using (var command = dbCommandFactory()) { - command.CommandText = GetExecutedScriptsSql(_schema, _table); + command.CommandText = GetExecutedScriptsSql(); command.CommandType = CommandType.Text; using (var reader = command.ExecuteReader()) @@ -146,102 +124,9 @@ public Dictionary GetExecutedScriptDictionary() /// /// Create an SQL statement which will retrieve all executed scripts in order. /// - protected virtual string GetExecutedScriptsSql(string schema, string table) - { - return $"select [ScriptName], [ScriptHash] from {CreateTableName(schema, table)} order by [ScriptName]"; - } - - /// Generates an SQL statement that, when exectuted, will create the journal database table. - /// Desired schema name supplied by configuration or NULL - /// Desired table name - /// A CREATE TABLE SQL statement - protected virtual string CreateTableSql(string schema, string table) - { - var tableName = CreateTableName(schema, table); - var primaryKeyConstraintName = CreatePrimaryKeyName(table); - - return $@"create table {tableName} ( - [Id] int identity(1,1) not null constraint {primaryKeyConstraintName} primary key, - [ScriptName] nvarchar(255) not null, - [Applied] datetime not null, - [ScriptHash] nvarchar(255) null -)"; - } - - /// - /// Combine the schema and table values into an appropriately-quoted identifier for the journal - /// table. - /// - /// Desired schema name supplied by configuration or NULL - /// Desired table name - /// Quoted journal table identifier - protected virtual string CreateTableName(string schema, string table) - { - return string.IsNullOrEmpty(schema) - ? SqlObjectParser.QuoteSqlObjectName(table) - : SqlObjectParser.QuoteSqlObjectName(schema) + "." + SqlObjectParser.QuoteSqlObjectName(table); - } - - /// - /// Convert the table value into an appropriately-quoted identifier for the journal table's unique primary - /// key. - /// - /// Desired table name - /// Quoted journal table primary key identifier - protected virtual string CreatePrimaryKeyName(string table) + protected virtual string GetExecutedScriptsSql() { - return SqlObjectParser.QuoteSqlObjectName("PK_" + table + "_Id"); + return $"select [ScriptName], [ScriptHash] from {FqSchemaTableName} order by [ScriptName]"; } - - private bool DoesTableExist() - { - return _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => - { - try - { - using (var command = dbCommandFactory()) - { - return VerifyTableExistsCommand(command, _table, _schema); - } - } - catch (SqlException) - { - return false; - } - catch (DbException) - { - return false; - } - }); - } - protected void EnsureHashColumnExists() - { - _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => - { - using (var command = dbCommandFactory()) - { - command.CommandText = - $"if not exists (select column_name from INFORMATION_SCHEMA.columns where table_name = '{_table}' and column_name = 'ScriptHash') alter table {_table} add ScriptHash nvarchar(255)"; - command.CommandType = CommandType.Text; - command.ExecuteNonQuery(); - } - }); - } - - /// Verify, using database-specific queries, if the table exists in the database. - /// The IDbCommand to be used for the query - /// The name of the table - /// The schema for the table - /// True if table exists, false otherwise - protected virtual bool VerifyTableExistsCommand(IDbCommand command, string tableName, string schemaName) - { - command.CommandText = string.IsNullOrEmpty(_schema) - ? $"select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME = '{tableName}'" - : $"select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME = '{tableName}' and TABLE_SCHEMA = '{schemaName}'"; - command.CommandType = CommandType.Text; - var result = command.ExecuteScalar() as int?; - return result == 1; - } - } -} \ No newline at end of file +} diff --git a/Source/Improving.DbUp/Hashed/HashedSqlTableJournalOld.cs b/Source/Improving.DbUp/Hashed/HashedSqlTableJournalOld.cs new file mode 100644 index 0000000..b3ca80f --- /dev/null +++ b/Source/Improving.DbUp/Hashed/HashedSqlTableJournalOld.cs @@ -0,0 +1,247 @@ +namespace Improving.DbUp.Hashed +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Data.Common; + using System.Data.SqlClient; + using global::DbUp.Engine; + using global::DbUp.Engine.Output; + using global::DbUp.Engine.Transactions; + using global::DbUp.Support.SqlServer; + + public class HashedSqlTableJournalOld : IJournal + { + private readonly Func _connectionManager; + private readonly Func _log; + private readonly string _schema; + private readonly string _table; + + /// + /// Initializes a new instance of the class. + /// + /// The connection manager. + /// The log. + /// The schema that contains the table. + /// The table name. + public HashedSqlTableJournalOld( + Func connectionManager, + Func logger, + string schema, + string table) + { + _schema = schema; + _table = table; + + _connectionManager = connectionManager; + + _log = logger; + } + + /// + /// Recalls the version number of the database. + /// + /// All executed scripts. + public string[] GetExecutedScripts() + { + // note: the HashedEmbeddedScriptsProvider implementation will deal with "already/should" run determination so we don't want any "executed scripts" in the pipeline + return new string[0]; + } + + /// + /// Records a database upgrade for a database specified in a given connection string. + /// + /// The script. + public void StoreExecutedScript(SqlScript script) + { + var exists = DoesTableExist(); + if (!exists) + { + _log().WriteInformation($"Creating the {CreateTableName(_schema, _table)} table"); + + _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => + { + using (var command = dbCommandFactory()) + { + command.CommandText = CreateTableSql(_schema, _table); + + command.CommandType = CommandType.Text; + command.ExecuteNonQuery(); + } + + _log() + .WriteInformation($"The {CreateTableName(_schema, _table)} table has been created"); + }); + } + EnsureHashColumnExists(); + + _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => + { + using (var command = dbCommandFactory()) + { + command.CommandText = + $@" + MERGE {CreateTableName(_schema, _table)} AS [Target] + USING(SELECT @scriptName as scriptName, @applied as applied, @scriptHash as scriptHash) AS [Source] + ON [Target].scriptName = [Source].scriptName + WHEN MATCHED THEN + UPDATE SET[Target].applied = [Source].applied, [Target].scriptHash = [Source].scriptHash + WHEN NOT MATCHED THEN + INSERT(ScriptName, Applied, ScriptHash) VALUES([Source].scriptName, [Source].applied, [Source].scriptHash); "; + + // todo: strip off the |HASH" from Name: + var scriptNameParam = command.CreateParameter(); + scriptNameParam.ParameterName = "scriptName"; + scriptNameParam.Value = script.Name; + command.Parameters.Add(scriptNameParam); + + var appliedParam = command.CreateParameter(); + appliedParam.ParameterName = "applied"; + appliedParam.Value = DateTime.Now; + command.Parameters.Add(appliedParam); + + var hashParam = command.CreateParameter(); + hashParam.ParameterName = "scriptHash"; + hashParam.Value = Md5Utils.Md5EncodeString(script.Contents); + command.Parameters.Add(hashParam); + + command.CommandType = CommandType.Text; + command.ExecuteNonQuery(); + } + }); + } + + public Dictionary GetExecutedScriptDictionary() + { + _log().WriteInformation("Fetching list of already executed scripts with their known hash."); + var exists = DoesTableExist(); + if (!exists) + { + _log() + .WriteInformation( + $"The {CreateTableName(_schema, _table)} table could not be found. The database is assumed to be at version 0."); + return new Dictionary(); + } + EnsureHashColumnExists(); + + var scripts = new Dictionary(); + _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => + { + using (var command = dbCommandFactory()) + { + command.CommandText = GetExecutedScriptsSql(_schema, _table); + command.CommandType = CommandType.Text; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + scripts.Add((string)reader[0], reader[1] == DBNull.Value ? string.Empty : (string)reader[1]); + } + } + }); + + return scripts; + } + + /// + /// Create an SQL statement which will retrieve all executed scripts in order. + /// + protected virtual string GetExecutedScriptsSql(string schema, string table) + { + return $"select [ScriptName], [ScriptHash] from {CreateTableName(schema, table)} order by [ScriptName]"; + } + + /// Generates an SQL statement that, when exectuted, will create the journal database table. + /// Desired schema name supplied by configuration or NULL + /// Desired table name + /// A CREATE TABLE SQL statement + protected virtual string CreateTableSql(string schema, string table) + { + var tableName = CreateTableName(schema, table); + var primaryKeyConstraintName = CreatePrimaryKeyName(table); + + return $@"create table {tableName} ( + [Id] int identity(1,1) not null constraint {primaryKeyConstraintName} primary key, + [ScriptName] nvarchar(255) not null, + [Applied] datetime not null, + [ScriptHash] nvarchar(255) null +)"; + } + + /// + /// Combine the schema and table values into an appropriately-quoted identifier for the journal + /// table. + /// + /// Desired schema name supplied by configuration or NULL + /// Desired table name + /// Quoted journal table identifier + protected virtual string CreateTableName(string schema, string table) + { + return string.IsNullOrEmpty(schema) + ? SqlObjectParser.QuoteSqlObjectName(table) + : SqlObjectParser.QuoteSqlObjectName(schema) + "." + SqlObjectParser.QuoteSqlObjectName(table); + } + + /// + /// Convert the table value into an appropriately-quoted identifier for the journal table's unique primary + /// key. + /// + /// Desired table name + /// Quoted journal table primary key identifier + protected virtual string CreatePrimaryKeyName(string table) + { + return SqlObjectParser.QuoteSqlObjectName("PK_" + table + "_Id"); + } + + private bool DoesTableExist() + { + return _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => + { + try + { + using (var command = dbCommandFactory()) + { + return VerifyTableExistsCommand(command, _table, _schema); + } + } + catch (SqlException) + { + return false; + } + catch (DbException) + { + return false; + } + }); + } + protected void EnsureHashColumnExists() + { + _connectionManager().ExecuteCommandsWithManagedConnection(dbCommandFactory => + { + using (var command = dbCommandFactory()) + { + command.CommandText = + $"if not exists (select column_name from INFORMATION_SCHEMA.columns where table_name = '{_table}' and column_name = 'ScriptHash') alter table {_table} add ScriptHash nvarchar(255)"; + command.CommandType = CommandType.Text; + command.ExecuteNonQuery(); + } + }); + } + + /// Verify, using database-specific queries, if the table exists in the database. + /// The IDbCommand to be used for the query + /// The name of the table + /// The schema for the table + /// True if table exists, false otherwise + protected virtual bool VerifyTableExistsCommand(IDbCommand command, string tableName, string schemaName) + { + command.CommandText = string.IsNullOrEmpty(_schema) + ? $"select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME = '{tableName}'" + : $"select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME = '{tableName}' and TABLE_SCHEMA = '{schemaName}'"; + command.CommandType = CommandType.Text; + var result = command.ExecuteScalar() as int?; + return result == 1; + } + + } +} \ No newline at end of file diff --git a/Source/Improving.DbUp/Hashed/IHashedJournal.cs b/Source/Improving.DbUp/Hashed/IHashedJournal.cs new file mode 100644 index 0000000..45ab396 --- /dev/null +++ b/Source/Improving.DbUp/Hashed/IHashedJournal.cs @@ -0,0 +1,14 @@ +using DbUp.Engine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Improving.DbUp.Hashed +{ + public interface IHashedJournal : IJournal + { + Dictionary GetExecutedScriptDictionary(); + } +} diff --git a/Source/Improving.DbUp/Improving.DbUp.csproj b/Source/Improving.DbUp/Improving.DbUp.csproj index 7d5832d..1835dce 100644 --- a/Source/Improving.DbUp/Improving.DbUp.csproj +++ b/Source/Improving.DbUp/Improving.DbUp.csproj @@ -31,9 +31,11 @@ 4 - - ..\..\packages\dbup.3.3.5\lib\net35\DbUp.dll - True + + ..\..\packages\dbup-core.4.2.0\lib\net45\dbup-core.dll + + + ..\..\packages\dbup-sqlserver.4.2.0\lib\net35\dbup-sqlserver.dll @@ -50,10 +52,13 @@ Properties\GlobalAssemblyInfo.cs + - + + + @@ -61,10 +66,10 @@ Always - Designer + diff --git a/Source/Improving.DbUp/Improving.DbUp.nuspec b/Source/Improving.DbUp/Improving.DbUp.nuspec index 6eff2de..4497310 100644 --- a/Source/Improving.DbUp/Improving.DbUp.nuspec +++ b/Source/Improving.DbUp/Improving.DbUp.nuspec @@ -1,10 +1,10 @@  - Improving.DbUp + Improving.DbUp.FileSystem $version$ $title$ - Craig Neuwirt,Michael Dudley,Cori Drew + Craig Neuwirt,Michael Dudley,Cori Drew,Rumit Parakhiya Improving false $description$ diff --git a/Source/Improving.DbUp/packages.config b/Source/Improving.DbUp/packages.config index bbab3dd..8174a36 100644 --- a/Source/Improving.DbUp/packages.config +++ b/Source/Improving.DbUp/packages.config @@ -1,4 +1,5 @@  - + + \ No newline at end of file