diff --git a/Tests/Extensions.cs b/Tests/Extensions.cs index ab9d47b..aff4133 100644 --- a/Tests/Extensions.cs +++ b/Tests/Extensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using dotMigrator; namespace Tests diff --git a/Tests/FakeJournal.cs b/Tests/FakeJournal.cs index 4ea8562..e10ab30 100644 --- a/Tests/FakeJournal.cs +++ b/Tests/FakeJournal.cs @@ -37,13 +37,15 @@ public void CreateJournal() IsCreated = true; } - public void SetBaseline(IEnumerable baselineMigrations) + public IReadOnlyList SetBaseline(IEnumerable baselineMigrations) { CreateJournal(); foreach (var migration in baselineMigrations) { - _migrations.Add(migration.Name, new DeployedMigration(migration.MigrationNumber, migration.Name, migration.Fingerprint, true)); + var deployedMigration = new DeployedMigration(migration.MigrationNumber, migration.Name, migration.Fingerprint, true); + _migrations.Add(migration.Name, deployedMigration); } + return _migrations.Values.OrderBy(m => m.MigrationNumber).ToList(); } public void RecordStartMigration(Migration migrationToRun) diff --git a/dotMigrator.SqlServer/ConnectionProperties.cs b/dotMigrator.SqlServer/ConnectionProperties.cs new file mode 100644 index 0000000..d366660 --- /dev/null +++ b/dotMigrator.SqlServer/ConnectionProperties.cs @@ -0,0 +1,107 @@ +using System.Data.SqlClient; + +namespace dotMigrator.SqlServer +{ + /// + /// A class to encapsulate the properties of a Sql Server connection. + /// + public class ConnectionProperties + { + /// + /// A connection string suitable for the SqlConnection object. + /// + public string ConnectionString { get; } + + /// + /// The server and/or instance name of the SQL server to connect to + /// + public string ServerInstance { get; } + + /// + /// The name of the database to connect to ("Initial Catalog") + /// + public string TargetDatabaseName { get; } + + /// + /// The SQL authentication user name if not using windows integrated security + /// + public string SqlUserName { get; } + + /// + /// The SQL authentication password if not using windows integrated security + /// + public string SqlUserPassword { get; } + + /// + /// Indicates whether to use windows integrated security. When this is true, the SqlUserName and SqlUserPassword are ignored. + /// + public bool UseWindowsIntegratedSecurity { get; } + + /// + /// Constructor to use when the individual connection property values are available. + /// + /// + /// + /// + /// + /// + public ConnectionProperties( + string serverInstance, + string targetDatabaseName, + string sqlUserName = null, + string sqlUserPassword = null, + bool useWindowsIntegratedSecurity = true) + { + ServerInstance = serverInstance; + TargetDatabaseName = targetDatabaseName; + SqlUserName = sqlUserName; + SqlUserPassword = sqlUserPassword; + UseWindowsIntegratedSecurity = useWindowsIntegratedSecurity; + + var connectionBuilder = new SqlConnectionStringBuilder + { + DataSource = ServerInstance, + ApplicationName = "dotMigrator", + InitialCatalog = TargetDatabaseName, + }; + if (UseWindowsIntegratedSecurity) + { + connectionBuilder.IntegratedSecurity = true; + } + else + { + connectionBuilder.UserID = SqlUserName; + connectionBuilder.Password = SqlUserPassword; + } + ConnectionString = connectionBuilder.ConnectionString; + } + + /// + /// Constructor to use when specific connection string property values are needed, or when + /// a connection string is more easily available than the individual connection properties + /// + /// + public ConnectionProperties(string connectionString) + { + ConnectionString = connectionString; + var bldr = new SqlConnectionStringBuilder(connectionString); + ServerInstance = bldr.DataSource; + TargetDatabaseName = bldr.InitialCatalog; + SqlUserName = bldr.UserID; + SqlUserPassword = bldr.Password; + UseWindowsIntegratedSecurity = bldr.IntegratedSecurity; + } + + /// + /// Creates and opens a new SqlConnection to the target database + /// + /// + public SqlConnection OpenConnection() + { + var connection = new SqlConnection(ConnectionString); + connection.Open(); + return connection; + + } + } +} \ No newline at end of file diff --git a/dotMigrator.SqlServerTableJournal/Properties/AssemblyInfo.cs b/dotMigrator.SqlServer/Properties/AssemblyInfo.cs similarity index 85% rename from dotMigrator.SqlServerTableJournal/Properties/AssemblyInfo.cs rename to dotMigrator.SqlServer/Properties/AssemblyInfo.cs index c2eb818..ceb19a3 100644 --- a/dotMigrator.SqlServerTableJournal/Properties/AssemblyInfo.cs +++ b/dotMigrator.SqlServer/Properties/AssemblyInfo.cs @@ -5,11 +5,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("dotMigrator.SqlServerTableJournal")] +[assembly: AssemblyTitle("dotMigrator.SqlServer")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("dotMigrator.SqlServerTableJournal")] +[assembly: AssemblyProduct("dotMigrator.SqlServer")] [assembly: AssemblyCopyright("Copyright © 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.*")] -[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: AssemblyVersion("0.2.*")] +[assembly: AssemblyFileVersion("0.2.0.0")] diff --git a/dotMigrator.SqlServerTableJournal/SqlServerTableJournal.cs b/dotMigrator.SqlServer/SingleTableJournal.cs similarity index 69% rename from dotMigrator.SqlServerTableJournal/SqlServerTableJournal.cs rename to dotMigrator.SqlServer/SingleTableJournal.cs index f7c84cf..f823fe8 100644 --- a/dotMigrator.SqlServerTableJournal/SqlServerTableJournal.cs +++ b/dotMigrator.SqlServer/SingleTableJournal.cs @@ -2,16 +2,16 @@ using System.Collections.Generic; using System.Data; using System.Data.SqlClient; +using System.Linq; -namespace dotMigrator.SqlServerTableJournal +namespace dotMigrator.SqlServer { - public class SqlServerTableJournal : IJournal, IDisposable + /// + /// Holds the journal for both migrations and stored code definitions in a single table + /// + public class SingleTableJournal : IJournal, IDisposable { - private readonly string _serverInstance; - private readonly string _targetDatabaseName; - private readonly string _sqlUserName; - private readonly string _sqlUserPassword; - private readonly bool _useWindowsIntegratedSecurity; + private readonly ConnectionProperties _connectionProperties; private readonly IProgressReporter _progressReporter; private SqlConnection _connection; @@ -20,39 +20,28 @@ public class SqlServerTableJournal : IJournal, IDisposable private SqlCommand _upsertCommand; private SqlCommand _setCompleteCommand; - public SqlServerTableJournal( - string serverInstance, - string targetDatabaseName, - string sqlUserName, - string sqlUserPassword, - bool useWindowsIntegratedSecurity, + /// + /// Constructs the journal object which will use the supplied connection properties and holds the progressReporter without connecting to the database + /// + /// + /// + public SingleTableJournal( + ConnectionProperties connectionProperties, IProgressReporter progressReporter) { - _serverInstance = serverInstance; - _sqlUserName = sqlUserName; - _sqlUserPassword = sqlUserPassword; - _useWindowsIntegratedSecurity = useWindowsIntegratedSecurity; + _connectionProperties = connectionProperties; _progressReporter = progressReporter; - _targetDatabaseName = targetDatabaseName; } + /// + /// Connects to the target database and prepares to read and write from the journal table "_DeployedScripts" + /// public void Open() { if (_connection != null) return; - // open our Sql connection and look for the _DeployedScripts table - var connectionBuilder = new SqlConnectionStringBuilder { DataSource = _serverInstance, ApplicationName = "dotMigrator", InitialCatalog = _targetDatabaseName }; - if (_useWindowsIntegratedSecurity) - { - connectionBuilder.IntegratedSecurity = true; - } - else - { - connectionBuilder.UserID = _sqlUserName; - connectionBuilder.Password = _sqlUserPassword; - } - _connection = new SqlConnection(connectionBuilder.ConnectionString); - _connection.Open(); + + _connection = _connectionProperties.OpenConnection(); _selectCommand = _connection.CreateCommand(); _selectCommand.CommandText = @@ -107,6 +96,9 @@ public void Open() _setCompleteCommand.Parameters.Add("@CompletedTs", SqlDbType.DateTime2); } + /// + /// Ensures the journal table "[dbo].[_DeployedScripts]" is present in the target database, creating it if necessary. + /// public void CreateJournal() { var findTableCommand = new SqlCommand("SELECT OBJECT_ID('_DeployedScripts')", _connection); @@ -124,17 +116,25 @@ [Fingerprint] [nvarchar](50) NOT NULL )", _connection); - _progressReporter.Report($"Preparing database \"{_targetDatabaseName}\" for future deployments..."); + _progressReporter.Report($"Preparing database \"{_connectionProperties.TargetDatabaseName}\" for future deployments..."); createTableCommand.ExecuteNonQuery(); _progressReporter.Report("Done"); } } - public void SetBaseline(IEnumerable baselineMigrations) + /// + /// Records that a series of migrations has already been completed in a target data store. + /// This is used when an existing data store is being put under management by dotMigrator + /// + /// + /// + public IReadOnlyList SetBaseline(IEnumerable baselineMigrations) { // first we'll call CreateJournal to ensure the table is already set up. CreateJournal(); + var toReturn = new List(); + foreach (var migration in baselineMigrations) { // insert into our table @@ -145,9 +145,16 @@ public void SetBaseline(IEnumerable baselineMigrations) _insertCommand.Parameters["@CompletedTs"].Value = DateTime.Now; _insertCommand.Parameters["@Fingerprint"].Value = migration.Fingerprint; _insertCommand.ExecuteNonQuery(); + toReturn.Add(new DeployedMigration(migration.MigrationNumber, migration.Name, migration.Fingerprint, true)); } + return toReturn.OrderBy(m => m.MigrationNumber).ToList(); } + /// + /// Insert or update the migration identified by its name in the journal. + /// It will be recorded as an incomplete migration. + /// + /// public void RecordStartMigration(Migration migration) { // insert or update the fingerprint of a migration. @@ -160,6 +167,11 @@ public void RecordStartMigration(Migration migration) _upsertCommand.ExecuteNonQuery(); } + /// + /// Update the identified migration in the journal as having been completed. + /// It can be assumed that this will only be called for the most recently started mgiration. + /// + /// public void RecordCompleteMigration(Migration migration) { _setCompleteCommand.Parameters["@Name"].Value = migration.Name; @@ -167,6 +179,13 @@ public void RecordCompleteMigration(Migration migration) _setCompleteCommand.ExecuteNonQuery(); } + /// + /// Insert a record to the journal that a new stored code definition has been completely applied, + /// or update an existing record with the new fingerprint of a stored code definition that has + /// just been applied. + /// + /// + /// public void RecordStoredCodeDefinition(StoredCodeDefinition storedCodeDefinition, int lastMigrationNumber) { // insert or update the fingerprint of a stored code definition. @@ -179,6 +198,9 @@ public void RecordStoredCodeDefinition(StoredCodeDefinition storedCodeDefinition _upsertCommand.ExecuteNonQuery(); } + /// + /// Returns the sequenced list of offline and online migrations that have been recorded in the journal + /// public IReadOnlyList GetDeployedMigrations() { var toReturn = new List(); @@ -198,6 +220,12 @@ public IReadOnlyList GetDeployedMigrations() return toReturn; } + /// + /// Returns all of the stored code definitions that have been previously applied as recorded in this journal. + /// The order of the definitions does not matter since the names are used to match them up with the available ones + /// that the MigrationsProvider finds. + /// + /// public IReadOnlyList GetDeployedStoredCodeDefinitions() { var toReturn = new List(); @@ -217,9 +245,13 @@ public IReadOnlyList GetDeployedStoredCodeDefiniti return toReturn; } + /// + /// Close the connection to the database + /// public void Dispose() { _connection?.Dispose(); + _connection = null; _selectCommand?.Dispose(); _insertCommand?.Dispose(); _upsertCommand?.Dispose(); diff --git a/dotMigrator.SqlServerTableJournal/dotMigrator.SqlServerTableJournal.csproj b/dotMigrator.SqlServer/dotMigrator.SqlServer.csproj similarity index 84% rename from dotMigrator.SqlServerTableJournal/dotMigrator.SqlServerTableJournal.csproj rename to dotMigrator.SqlServer/dotMigrator.SqlServer.csproj index 4ac80f5..2442e98 100644 --- a/dotMigrator.SqlServerTableJournal/dotMigrator.SqlServerTableJournal.csproj +++ b/dotMigrator.SqlServer/dotMigrator.SqlServer.csproj @@ -7,8 +7,8 @@ {BEE4497E-6BDF-4A8F-A8A5-D3CCC0F2F74B} Library Properties - dotMigrator.SqlServerTableJournal - dotMigrator.SqlServerTableJournal + dotMigrator.SqlServer + dotMigrator.SqlServer v4.5 512 @@ -28,7 +28,7 @@ TRACE prompt 4 - bin\Release\dotMigrator.SqlServerTableJournal.xml + bin\Release\dotMigrator.SqlServer.xml @@ -37,7 +37,8 @@ - + + @@ -47,4 +48,5 @@ + \ No newline at end of file diff --git a/dotMigrator.SqlServer2012.SmoScriptRunner/Properties/AssemblyInfo.cs b/dotMigrator.SqlServer2012.SmoScriptRunner/Properties/AssemblyInfo.cs index f450afa..316c355 100644 --- a/dotMigrator.SqlServer2012.SmoScriptRunner/Properties/AssemblyInfo.cs +++ b/dotMigrator.SqlServer2012.SmoScriptRunner/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.*")] -[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: AssemblyVersion("0.2.*")] +[assembly: AssemblyFileVersion("0.2.0.0")] diff --git a/dotMigrator.SqlServer2012.SmoScriptRunner/ScriptRunner.cs b/dotMigrator.SqlServer2012.SmoScriptRunner/ScriptRunner.cs index 09ae534..a149790 100644 --- a/dotMigrator.SqlServer2012.SmoScriptRunner/ScriptRunner.cs +++ b/dotMigrator.SqlServer2012.SmoScriptRunner/ScriptRunner.cs @@ -1,57 +1,64 @@ using System; +using dotMigrator.SqlServer; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; namespace dotMigrator.SqlServer2012.SmoScriptRunner { + /// + /// Uses SQL Server Managment Objects to run SQL scripts that may include multiple batches separated by "GO" statements. + /// Scripts run by this runner should not have a "USE [db]" statement, since this runner will switch to the + /// indicated target database before running each script. + /// public class ScriptRunner : IScriptRunner, IDisposable { - private readonly string _serverInstance; - private readonly string _sqlUserName; - private readonly string _sqlUserPassword; - private readonly bool _useWindowsIntegratedSecurity; - private readonly string _targetDatabaseName; + private readonly ConnectionProperties _connectionProperties; private Database _database; private Server _server; + /// + /// Constructs a ScriptRunner that can run scripts in the database identified in the given connectionProperties + /// + /// public ScriptRunner( - string serverInstance, - string targetDatabaseName, - string sqlUserName, - string sqlUserPassword, - bool useWindowsIntegratedSecurity) + ConnectionProperties connectionProperties) { - _serverInstance = serverInstance; - _sqlUserName = sqlUserName; - _sqlUserPassword = sqlUserPassword; - _useWindowsIntegratedSecurity = useWindowsIntegratedSecurity; - _targetDatabaseName = targetDatabaseName; + _connectionProperties = connectionProperties; } - + /// + /// Opens a connection to the target database and verifies its existence so that it's ready to execute scripts + /// public void Open() { ServerConnection serverConnection = - _useWindowsIntegratedSecurity - ? new ServerConnection(_serverInstance) - : new ServerConnection(_serverInstance, _sqlUserName, _sqlUserPassword); + _connectionProperties.UseWindowsIntegratedSecurity + ? new ServerConnection(_connectionProperties.ServerInstance) + : new ServerConnection(_connectionProperties.ServerInstance, _connectionProperties.SqlUserName, _connectionProperties.SqlUserPassword); _server = new Server(serverConnection); _server.ConnectionContext.Connect(); - _database = _server.Databases[_targetDatabaseName]; + _database = _server.Databases[_connectionProperties.TargetDatabaseName]; if (_database == null) { - throw new Exception($"Database {_targetDatabaseName} does not exist at target {_serverInstance}"); + throw new Exception($"Database {_connectionProperties.TargetDatabaseName} does not exist on server {_connectionProperties.ServerInstance}"); } } + /// + /// Executes the given SQL script in the target database. The scriptToRun should not have a "USE [db]" statement. + /// + /// public void Run(string scriptToRun) { // we'll always run the script in the prescribed database _database.ExecuteNonQuery(scriptToRun); } + /// + /// Disconnects from the SQL Server + /// public void Dispose() { _server.ConnectionContext.Disconnect(); diff --git a/dotMigrator.SqlServer2012.SmoScriptRunner/dotMigrator.SqlServer2012.SmoScriptRunner.csproj b/dotMigrator.SqlServer2012.SmoScriptRunner/dotMigrator.SqlServer2012.SmoScriptRunner.csproj index 6a70c3c..b92b530 100644 --- a/dotMigrator.SqlServer2012.SmoScriptRunner/dotMigrator.SqlServer2012.SmoScriptRunner.csproj +++ b/dotMigrator.SqlServer2012.SmoScriptRunner/dotMigrator.SqlServer2012.SmoScriptRunner.csproj @@ -56,10 +56,15 @@ + + {bee4497e-6bdf-4a8f-a8a5-d3ccc0f2f74b} + dotMigrator.SqlServer + {93c85347-5682-4f48-9168-9ad0a5edc7e3} dotMigrator + \ No newline at end of file diff --git a/dotMigrator.sln b/dotMigrator.sln index 76d1d28..4a53b51 100644 --- a/dotMigrator.sln +++ b/dotMigrator.sln @@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotMigrator.SqlServer2012.S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotMigrator", "dotMigrator\dotMigrator.csproj", "{93C85347-5682-4F48-9168-9AD0A5EDC7E3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotMigrator.SqlServerTableJournal", "dotMigrator.SqlServerTableJournal\dotMigrator.SqlServerTableJournal.csproj", "{BEE4497E-6BDF-4A8F-A8A5-D3CCC0F2F74B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotMigrator.SqlServer", "dotMigrator.SqlServer\dotMigrator.SqlServer.csproj", "{BEE4497E-6BDF-4A8F-A8A5-D3CCC0F2F74B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{43C20DB6-50E4-4112-9220-FFE0609C1EAC}" EndProject diff --git a/dotMigrator.targets b/dotMigrator.targets new file mode 100644 index 0000000..f48269b --- /dev/null +++ b/dotMigrator.targets @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotMigrator/ConsoleProgressReporter.cs b/dotMigrator/ConsoleProgressReporter.cs new file mode 100644 index 0000000..723abe2 --- /dev/null +++ b/dotMigrator/ConsoleProgressReporter.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace dotMigrator +{ + public class ConsoleProgressReporter : IProgressReporter + { + private readonly Stack _blockNames = new Stack(); + + public void Report(string message) + { + Console.Write(new string(' ', _blockNames.Count * 3)); + Console.WriteLine(message); + } + + public void BeginBlock(string name) + { + Console.Write(new string(' ', _blockNames.Count * 3)); + Console.WriteLine("Begin " + name); + _blockNames.Push(name); + } + + public void EndBlock(string name) + { + Console.Write(new string(' ', _blockNames.Count * 3)); + Console.WriteLine("End " + name); + + //now we pop the block stack until we find the block we're looking for + var undo = new Stack(); + while (_blockNames.Count > 0) + { + var lastBlock = _blockNames.Pop(); + if (lastBlock == name) + return; + } + // if we didn't find a matching block name, put all of them back + while(undo.Count > 0) + _blockNames.Push(undo.Pop()); + } + } + + public class TeamCityProgressReporter : IProgressReporter + { + public void Report(string message) + { + Console.WriteLine($"##teamcity[message text='{Escape(message)}']"); + } + + public void BeginBlock(string name) + { + Console.WriteLine($"##teamcity[blockOpened name='{Escape(name)}']"); + } + + public void EndBlock(string name) + { + Console.WriteLine($"##teamcity[blockClosed name='{Escape(name)}']"); + } + + private string Escape(string value) + { + return value + .Replace("'", "|'") + .Replace("\n", "|n") + .Replace("\r", "|r") + .Replace("[", "|[") + .Replace("]", "|]") + .Replace("|", "||"); + } + } +} \ No newline at end of file diff --git a/dotMigrator/DeployedMigration.cs b/dotMigrator/DeployedMigration.cs index 1d91217..94d26a9 100644 --- a/dotMigrator/DeployedMigration.cs +++ b/dotMigrator/DeployedMigration.cs @@ -1,7 +1,17 @@ namespace dotMigrator { + /// + /// Represents a migration that has already been deployed to the data store and recorded in its journal + /// public class DeployedMigration { + /// + /// Constructor + /// + /// Same as + /// Same as + /// Same as + /// Whether the migration has already run to completion public DeployedMigration( int migrationNumber, string name, @@ -14,9 +24,27 @@ public DeployedMigration( Complete = complete; } + /// + /// A unique non-negative integer that puts this migration in the sequence of all migrations for the data store + /// Same as + /// public int MigrationNumber { get; } + + /// + /// Uniquely identifies this migration in the available migrations from the and also in the journal + /// Same as + /// public string Name { get; } + + /// + /// Uniquely identifies the content of this migration in order to detect that it has changed bewteen deployments, so its outcome might be different + /// Same as + /// public string Fingerprint { get; } + + /// + /// Whether the migration ran to completion. + /// public bool Complete { get; } } } \ No newline at end of file diff --git a/dotMigrator/DeployedStoredCodeDefinition.cs b/dotMigrator/DeployedStoredCodeDefinition.cs index d80e76a..76b76a0 100644 --- a/dotMigrator/DeployedStoredCodeDefinition.cs +++ b/dotMigrator/DeployedStoredCodeDefinition.cs @@ -1,14 +1,31 @@ namespace dotMigrator { + /// + /// Represents a stored code definition that has already been applied to the data store and recorded in its journal + /// public class DeployedStoredCodeDefinition { + /// + /// Constructor + /// + /// + /// public DeployedStoredCodeDefinition(string name, string fingerprint) { Name = name; Fingerprint = fingerprint; } + /// + /// Uniquely identifies this definition within the journal associated with this data store. + /// The same as + /// public string Name { get; } + + /// + /// Uniquely identifies the content of this stored code definition to determine what has changed between deployments. Should be kept short. Typically a cryptographic hash. + /// The same as + /// public string Fingerprint { get; } } } \ No newline at end of file diff --git a/dotMigrator/MigrationPlan.cs b/dotMigrator/DeploymentPlan.cs similarity index 69% rename from dotMigrator/MigrationPlan.cs rename to dotMigrator/DeploymentPlan.cs index e69e385..5d44160 100644 --- a/dotMigrator/MigrationPlan.cs +++ b/dotMigrator/DeploymentPlan.cs @@ -5,9 +5,9 @@ namespace dotMigrator /// /// The result of /// - public class MigrationPlan + public class DeploymentPlan { - public MigrationPlan( + internal DeploymentPlan( string offlineErrorMessage, string onlineErrorMessage, IReadOnlyList offlineMigrations, @@ -23,10 +23,29 @@ public MigrationPlan( LastCompletedMigrationNumber = lastCompletedMigrationNumber; } + /// + /// Whether the plan includes offline migrations + /// public bool HasOfflineMigrations => OfflineMigrations.Count > 0; + + /// + /// Whether the plan includes stored code changes + /// public bool HasStoredCodeChanges => StoredCodeDefinitions.Count > 0; + + /// + /// Whether the plan includes online migrations + /// public bool HasOnlineMigrations => OnlineMigrations.Count > 0; + + /// + /// If dotMigrator cannot perform an offline deployment, this contains the error message + /// public string OfflineErrorMessage { get; } + + /// + /// If dotMigrator cannot perform an online deployment, this contains the error message + /// public string OnlineErrorMessage { get; } internal IReadOnlyList OfflineMigrations { get; } diff --git a/dotMigrator/Extensions.cs b/dotMigrator/Extensions.cs index 7049ca6..f1ea2ff 100644 --- a/dotMigrator/Extensions.cs +++ b/dotMigrator/Extensions.cs @@ -3,9 +3,9 @@ namespace dotMigrator { - public static class Extensions + internal static class Extensions { - public static IEnumerable TakeUntil(this IEnumerable source, Predicate predicate) + internal static IEnumerable TakeUntil(this IEnumerable source, Predicate predicate) { foreach (T el in source) { diff --git a/dotMigrator/FileSystemScriptsMigrationProvider.cs b/dotMigrator/FileSystemScriptsMigrationProvider.cs index 5ddbb78..970a690 100644 --- a/dotMigrator/FileSystemScriptsMigrationProvider.cs +++ b/dotMigrator/FileSystemScriptsMigrationProvider.cs @@ -7,6 +7,10 @@ namespace dotMigrator { + /// + /// Reads text scripts from folders in the filesystem. Migrations from one path and stored code definitions from another. + /// Fingerprints are the MD5 hash of the UTF-8 encoding of the textual content of the files even if the files are actually stored in another encoding. + /// public class FileSystemScriptsMigrationProvider : IMigrationsProvider { private readonly string _migrationsDirectory; @@ -15,6 +19,14 @@ public class FileSystemScriptsMigrationProvider : IMigrationsProvider private readonly string _storedCodeDefinitionFileWildcard; private readonly IScriptRunner _scriptRunner; + /// + /// Construct a FileSystemScriptsMigrationProvider using the given directories and glob patterns. + /// + /// The directory containing migration scripts + /// The filename pattern to identify migration scripts in the directory + /// The directory containing stored code definition scripts + /// The filename pattern to identify the stored code definition scripts in the directory + /// The script runner that has the ability to execute the text of the scripts for the target data store public FileSystemScriptsMigrationProvider( string migrationsDirectory, string migrationsFileWildcard, @@ -29,6 +41,11 @@ public FileSystemScriptsMigrationProvider( _scriptRunner = scriptRunner; } + /// + /// Returns all of the known migrations in order by migration number by extracting the migration number from the digits in the beginning of the filename + /// Online migrations are identified by the string "online " following the migration number. + /// + /// public IReadOnlyList GatherMigrations() { MD5 md5 = MD5.Create(); @@ -85,13 +102,18 @@ public IReadOnlyList GatherMigrations() .ToList(); } + /// + /// Returns all of the known stored code definitions in the order in which they should be applied to the target data store. + /// Any digits from the front of the filename are used as the dependency level which deteremines the order in which they should be applied. + /// Those digits are not included in the name stored in the journal so that an object's dependency level can change without changing which object the file represents. + /// + /// public IReadOnlyList GatherStoredCodeDefinitions() { MD5 md5 = MD5.Create(); var files = Directory.GetFiles(_storedCodeDefinitionsDirectory, _storedCodeDefinitionFileWildcard); return files - .OrderBy(f => f) .Select( file => { @@ -101,21 +123,33 @@ public IReadOnlyList GatherStoredCodeDefinitions() if (string.IsNullOrEmpty(filename)) throw new Exception("Stored Code Definition script needs a filename"); + var digits = new Stack(filename.Length); for (int i = 0; i < filename.Length; i++) { char c = filename[i]; - if (!Char.IsDigit(c)) + + if (Char.IsDigit(c)) + digits.Push(c); + else { name = filename.Substring(i).Trim(); break; } } + int dependencyLevel = 0; + for (int placeValue = 1; digits.Count > 0; placeValue *= 10) + { + int digitValue = digits.Pop() - '0'; + dependencyLevel += digitValue * placeValue; + } var contents = new StreamReader(file, true).ReadToEnd(); var hash = BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(contents))); - return new StoredCodeDefinition(name, hash, pr => _scriptRunner.Run(contents)); + return new StoredCodeDefinition(name, hash, pr => _scriptRunner.Run(contents), dependencyLevel); } - ).ToList(); + ) + .OrderBy(c => c.DependencyLevel) + .ToList(); } } } \ No newline at end of file diff --git a/dotMigrator/IJournal.cs b/dotMigrator/IJournal.cs index ce57940..0c8b8dd 100644 --- a/dotMigrator/IJournal.cs +++ b/dotMigrator/IJournal.cs @@ -1,9 +1,10 @@ -using System; -using System.Collections; using System.Collections.Generic; namespace dotMigrator { + /// + /// The interface for classes that track the series of migrations and the versions of stored code definitions that have been deployed to a target data store. + /// public interface IJournal { /// @@ -12,22 +13,49 @@ public interface IJournal /// void CreateJournal(); - void SetBaseline(IEnumerable baselineMigrations); + /// + /// Records that a series of migrations has already been completed in a target data store. + /// This is used when an existing data store is being put under management by dotMigrator + /// + /// + /// + IReadOnlyList SetBaseline(IEnumerable baselineMigrations); /// - /// Insert or update the migration identified by its name. + /// Insert or update the migration identified by its name in the journal. /// It will be recorded as an incomplete migration. /// - /// - void RecordStartMigration(Migration migrationToRun); + /// + void RecordStartMigration(Migration migration); + + /// + /// Update the identified migration in the journal as having been completed. + /// It can be assumed that this will only be called for the most recently started mgiration. + /// + /// void RecordCompleteMigration(Migration migration); + + /// + /// Insert a record to the journal that a new stored code definition has been completely applied, + /// or update an existing record with the new fingerprint of a stored code definition that has + /// just been applied. + /// + /// + /// The number of the last migration to have completed when this stored code definition was applied void RecordStoredCodeDefinition(StoredCodeDefinition storedCodeDefinition, int lastMigrationNumber); + /// /// Returns the sequenced list of offline and online migrations that have been recorded in the journal /// - /// If the journal was never created for the target data store IReadOnlyList GetDeployedMigrations(); + + /// + /// Returns all of the stored code definitions that have been previously applied as recorded in this journal. + /// The order of the definitions does not matter since the names are used to match them up with the available ones + /// that the MigrationsProvider finds. + /// + /// IReadOnlyList GetDeployedStoredCodeDefinitions(); } } \ No newline at end of file diff --git a/dotMigrator/IMigrationsProvider.cs b/dotMigrator/IMigrationsProvider.cs index 5fc999f..d12afae 100644 --- a/dotMigrator/IMigrationsProvider.cs +++ b/dotMigrator/IMigrationsProvider.cs @@ -2,6 +2,9 @@ namespace dotMigrator { + /// + /// The interface for classes that gather all known migrations and stored code definitions that might need to be applied during a deployment + /// public interface IMigrationsProvider { /// diff --git a/dotMigrator/IProgressReporter.cs b/dotMigrator/IProgressReporter.cs index 13af44a..1f3d078 100644 --- a/dotMigrator/IProgressReporter.cs +++ b/dotMigrator/IProgressReporter.cs @@ -1,12 +1,18 @@ namespace dotMigrator { + /// + /// Interface for classes that can report progress to the user during deployments + /// public interface IProgressReporter { // methods such as "begin block", "end block", (like TeamCity progress messages) "writeline", maybe ways to report progress of a single 'task' in parts on one line // methods that might begin and then move a progress bar (like linux boot sequence) + void Report(string message); - void BeginBlock(string message); - void EndBlock(string message); + void BeginBlock(string name); + void EndBlock(string name); + + //TODO: some way to report percent complete or incrementing record counts } } \ No newline at end of file diff --git a/dotMigrator/IScriptRunner.cs b/dotMigrator/IScriptRunner.cs index 8d16418..739074d 100644 --- a/dotMigrator/IScriptRunner.cs +++ b/dotMigrator/IScriptRunner.cs @@ -1,7 +1,14 @@ namespace dotMigrator { + /// + /// Interface for classes that know how to run text-based scripts in a target data store + /// public interface IScriptRunner { + /// + /// Runs the given script in the target data store + /// + /// void Run(string scriptContents); } } \ No newline at end of file diff --git a/dotMigrator/MergedMigrationProvider.cs b/dotMigrator/MergedMigrationProvider.cs new file mode 100644 index 0000000..6b9e746 --- /dev/null +++ b/dotMigrator/MergedMigrationProvider.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace dotMigrator +{ + /// + /// A Migration provider that can combine migrations from multiple other providers. + /// + public class MergedMigrationProvider : IMigrationsProvider + { + private readonly List _providers = new List(); + + /// + /// Merges an additional migration provider + /// + /// + public void Add(IMigrationsProvider provider) + { + if(!_providers.Contains(provider)) + _providers.Add(provider); + } + + /// + /// Returns all of the known migrations in order by migration number + /// + /// + public IReadOnlyList GatherMigrations() + { + var toReturn = new List(); + foreach (var p in _providers) + toReturn.AddRange(p.GatherMigrations()); + toReturn.Sort((a,b) => a.MigrationNumber - b.MigrationNumber); // returns a negative number when "a" is less than "b" meaning "a" should sort before "b" + return toReturn; + } + + /// + /// Returns all of the known stored code definitions in the order in which they should be applied to the target data store + /// + /// + public IReadOnlyList GatherStoredCodeDefinitions() + { + var toReturn = new List(); + foreach (var p in _providers) + toReturn.AddRange(p.GatherStoredCodeDefinitions()); + toReturn.Sort((a, b) => a.DependencyLevel - b.DependencyLevel); // a definition with a lower dependency level should be applied before one with a higher dependency level + return toReturn; + } + } +} \ No newline at end of file diff --git a/dotMigrator/Migration.cs b/dotMigrator/Migration.cs index 14cb5b7..8a63b9a 100644 --- a/dotMigrator/Migration.cs +++ b/dotMigrator/Migration.cs @@ -2,33 +2,65 @@ namespace dotMigrator { + /// + /// Code or script that changes the data structure of data in the data store. + /// public class Migration { - private readonly Action _executeAction; + private readonly Action _runAction; + /// + /// Constructor + /// + /// A unique non-negative integer that puts this migration in the sequence of all migrations for the data store + /// A case-insensitive name to uniquely identify this migration. Could be a script filename. + /// A string that can be used to detect changes in the content between deployments. Typically a cryptographic hash of script file contents. + /// Indicates that the migration is written to run while the application remains online. + /// The delegate that performs the migration in the data store public Migration( int migrationNumber, string name, string fingerprint, bool isOnlineMigration, - Action executeAction + Action runAction ) { - _executeAction = executeAction; + _runAction = runAction; MigrationNumber = migrationNumber; Name = name; Fingerprint = fingerprint; IsOnlineMigration = isOnlineMigration; } + /// + /// A unique non-negative integer that puts this migration in the sequence of all migrations for the data store. + /// public int MigrationNumber { get; } + + /// + /// Uniquely identifies this migration in the available migrations from the and also in the journal. + /// public string Name { get; } + + /// + /// Uniquely identifies the content of this migration in order to detect that it has changed between + /// deployments, which means its outcome might be different. + /// public string Fingerprint { get; } + + /// + /// Indicates that this migration is expected to run while the application remains online. + /// It must be written so that it can resume where it left off in case it is interrupted and restarted. + /// public bool IsOnlineMigration { get; } - public void Execute(IProgressReporter progressReporter) + /// + /// Performs the migration in the target data store. + /// + /// + public void Run(IProgressReporter progressReporter) { - _executeAction.Invoke(progressReporter); + _runAction.Invoke(progressReporter); } } } \ No newline at end of file diff --git a/dotMigrator/Migrator.cs b/dotMigrator/Migrator.cs index 1386bfb..ed500ec 100644 --- a/dotMigrator/Migrator.cs +++ b/dotMigrator/Migrator.cs @@ -6,7 +6,7 @@ namespace dotMigrator { /// /// The main entrypoint class to dotMigrator. - /// Each instance of this class can be used to Plan and apply a set of migrations once. + /// Each instance of this class can be used to Plan and deploy a set of migrations once. /// public class Migrator { @@ -15,10 +15,17 @@ public class Migrator private readonly IProgressReporter _progressReporter; private readonly bool _includeOnlineMigrationsDuringOffline; - private MigrationPlan _migrationPlan; + private DeploymentPlan _deploymentPlan; private IReadOnlyList _deployedMigrations; private IReadOnlyList _availableMigrations; + /// + /// Create a migrator instance using the given dependencies + /// + /// + /// + /// + /// public Migrator( IJournal journal, IMigrationsProvider migrationsProvider, @@ -40,7 +47,7 @@ public void EnsureJournal() } /// - /// Populates the journal with the migrations that have already been applied + /// Populates the journal with the migrations that have already been deployed /// to the target database before dotMigrator was in use. /// /// @@ -48,13 +55,13 @@ public void EnsureBaseline(string baselineMigrationName) { EnsureJournal(); - var deployedMigrations = GetDeployedMigrations(); - if (deployedMigrations.Count == 0) + LoadDeployedMigrations(); + if (_deployedMigrations.Count == 0) { var baselineMigrations = GetAvailableMigrations() .TakeUntil(m => m.Name.Equals(baselineMigrationName, StringComparison.OrdinalIgnoreCase)); - _journal.SetBaseline(baselineMigrations); + _deployedMigrations = _journal.SetBaseline(baselineMigrations); } /* otherwise, the subset of available migrations up to the baselineMigrationName must * match the first deployed migrations.. but that will be checked when calling Plan() @@ -66,55 +73,57 @@ public void EnsureBaseline(string baselineMigrationName) /// if so, which of them need to run to bring it up-to-date /// /// - public MigrationPlan Plan() + public DeploymentPlan Plan() { - return _migrationPlan ?? (_migrationPlan = CreateMigrationPlan()); + return _deploymentPlan ?? (_deploymentPlan = CreateDeploymentPlan()); } /// - /// Applies all of the necessary offline migrations followed by the - /// changed stored code definitions + /// Runs all of the necessary offline migrations then applies + /// the stored code definitions that have changed /// - public void MigrateOffline() + public void DeployOffline() { Plan(); - if (_migrationPlan.OfflineErrorMessage != null) - throw new Exception(_migrationPlan.OfflineErrorMessage); + if (_deploymentPlan.OfflineErrorMessage != null) + throw new Exception(_deploymentPlan.OfflineErrorMessage); - var migrationNumberForStoredObjects = _migrationPlan.LastCompletedMigrationNumber; + var migrationNumberForStoredCode = _deploymentPlan.LastCompletedMigrationNumber; - if (_migrationPlan.OfflineMigrations.Any()) + if (_deploymentPlan.OfflineMigrations.Any()) { - _progressReporter.BeginBlock("Running offline migration scripts..."); - foreach (var migrationToRun in _migrationPlan.OfflineMigrations) + _progressReporter.BeginBlock("Offline Migrations"); + _progressReporter.Report("Running offline migration scripts..."); + foreach (var migrationToRun in _deploymentPlan.OfflineMigrations) { _progressReporter.Report($"Running {migrationToRun.Name} ..."); _journal.RecordStartMigration(migrationToRun); - migrationToRun.Execute(_progressReporter); - migrationNumberForStoredObjects = migrationToRun.MigrationNumber; + migrationToRun.Run(_progressReporter); + migrationNumberForStoredCode = migrationToRun.MigrationNumber; _journal.RecordCompleteMigration(migrationToRun); _progressReporter.Report("Done."); } - _progressReporter.EndBlock("Done."); + _progressReporter.EndBlock("Offline Migrations"); } else { _progressReporter.Report("No offline migrations to run."); } - if (_migrationPlan.HasStoredCodeChanges) + if (_deploymentPlan.HasStoredCodeChanges) { - _progressReporter.BeginBlock("Running stored code definitions..."); - foreach (var scriptToRun in _migrationPlan.StoredCodeDefinitions) + _progressReporter.BeginBlock("Stored Code Definitions"); + _progressReporter.Report("Running stored code definitions..."); + foreach (var definition in _deploymentPlan.StoredCodeDefinitions) { - _progressReporter.Report($"Running {scriptToRun.Name} ..."); - scriptToRun.Execute(_progressReporter); + _progressReporter.Report($"Running {definition.Name} ..."); + definition.Apply(_progressReporter); - // Record the fact that we ran that script. - _journal.RecordStoredCodeDefinition(scriptToRun, migrationNumberForStoredObjects); + // Record the fact that we applied that script. + _journal.RecordStoredCodeDefinition(definition, migrationNumberForStoredCode); _progressReporter.Report("Done."); } - _progressReporter.EndBlock("Done."); + _progressReporter.EndBlock("Stored Code Definitions"); } else { @@ -123,26 +132,27 @@ public void MigrateOffline() } /// - /// Applies all of the necessary online migrations + /// Runs all of the necessary online migrations /// - public void MigrateOnline() + public void DeployOnline() { Plan(); - if (_migrationPlan.OnlineErrorMessage != null) - throw new Exception(_migrationPlan.OnlineErrorMessage); + if (_deploymentPlan.OnlineErrorMessage != null) + throw new Exception(_deploymentPlan.OnlineErrorMessage); - if (_migrationPlan.HasOnlineMigrations) + if (_deploymentPlan.HasOnlineMigrations) { - _progressReporter.BeginBlock("Running online migration scripts..."); - foreach (var migrationToRun in _migrationPlan.OnlineMigrations) + _progressReporter.BeginBlock("Online Migrations"); + _progressReporter.Report("Running online migration scripts..."); + foreach (var migrationToRun in _deploymentPlan.OnlineMigrations) { _progressReporter.Report($"Running {migrationToRun.Name} ..."); _journal.RecordStartMigration(migrationToRun); - migrationToRun.Execute(_progressReporter); + migrationToRun.Run(_progressReporter); _journal.RecordCompleteMigration(migrationToRun); _progressReporter.Report("Done."); } - _progressReporter.EndBlock("Done."); + _progressReporter.EndBlock("Online Migrations"); } else { @@ -150,57 +160,57 @@ public void MigrateOnline() } } - private MigrationPlan CreateMigrationPlan() + private DeploymentPlan CreateDeploymentPlan() { - int lastAlreadyCompletedMigrationNumber = 0; + int lastCompletedMigrationNumber = 0; List offlineMigrationsToRun = new List(); List onlineMigrationsToRun = new List(); List storedCodeToRun = new List(); - MigrationPlan Error(string message) + DeploymentPlan Error(string message) { - return new MigrationPlan( + return new DeploymentPlan( message, message, offlineMigrationsToRun, storedCodeToRun, onlineMigrationsToRun, - lastAlreadyCompletedMigrationNumber); + lastCompletedMigrationNumber); } // the _journal might throw an exception here if it was never created for the target data store - var migrationsAlreadyRun = GetDeployedMigrations(); + LoadDeployedMigrations(); var availableMigrations = GetAvailableMigrations(); if (availableMigrations.GroupBy(am => am.Name, StringComparer.OrdinalIgnoreCase).Any(g => g.Count() > 1)) { // we have duplicate migration names so we can't do anything - return Error("Cannot migrate due to migration names that are not unique."); + return Error("Cannot deploy due to migration names that are not unique."); } if (availableMigrations.GroupBy(am => am.MigrationNumber).Any(g => g.Count() > 1)) { // we have duplicate migration numbers so we can't do anything - return Error("Cannot migrate due to migration numbers that are not unique."); + return Error("Cannot deploy due to migration numbers that are not unique."); } var migrationNumbers = availableMigrations.Select(am => am.MigrationNumber).ToArray(); if (!migrationNumbers.SequenceEqual(migrationNumbers.OrderBy(v => v))) { // we have migration numbers out-of order so we can't do anything - return Error("Cannot migrate due to migration numbers that are not in order."); + return Error("Cannot deploy due to migration numbers that are not in order."); } bool mustRestartLastMigration = false; - for (int i = 0; i < migrationsAlreadyRun.Count; i++) + for (int i = 0; i < _deployedMigrations.Count; i++) { - var deployedMigration = migrationsAlreadyRun[i]; + var deployedMigration = _deployedMigrations[i]; if (i == availableMigrations.Count) { // then we've just encountered the first deployed migration that is not known as an available one, so this is an attempt to migrate to an incompatible branch return Error( - "Cannot migrate due to incompatible branch. " + + "Cannot deploy due to incompatible branch. " + $"Deployed migration ({deployedMigration.MigrationNumber}) {deployedMigration.Name} is not known." ); } @@ -212,12 +222,12 @@ MigrationPlan Error(string message) // now if the migration is complete, the fingerprints must match if (deployedMigration.Complete) { - lastAlreadyCompletedMigrationNumber = deployedMigration.MigrationNumber; + lastCompletedMigrationNumber = deployedMigration.MigrationNumber; if (deployedMigration.Fingerprint != availableMigration.Fingerprint) { // then a completed migration has been modified, so this is an attempt to migrate to an incompatible branch return Error( - "Cannot migrate due to incompatible branch. " + + "Cannot deploy due to incompatible branch. " + $"Deployed migration ({deployedMigration.MigrationNumber}) {deployedMigration.Name} has been modified." ); } @@ -229,7 +239,7 @@ MigrationPlan Error(string message) if (availableMigration.IsOnlineMigration == false) { return Error( - "Cannot migrate due to incomplete prior offline migration. " + + "Cannot deploy due to incomplete prior offline migration. " + $"Deployed migration ({deployedMigration.MigrationNumber}) {deployedMigration.Name} did not complete. " + "Restore the database from a backup or manually fix the database and mark the migration complete in the journal, or delete it from the journal to have it run the next time." ); @@ -242,14 +252,14 @@ MigrationPlan Error(string message) { // then we just encountered a mismatch between the deployed and available migrations, so this is an attempt to migrate to an incompatible branch return Error( - "Cannot migrate due to incompatible branch. " + + "Cannot deploy due to incompatible branch. " + $"Available migration ({availableMigration.MigrationNumber}) {availableMigration.Name} was found where deployed migration ({deployedMigration.MigrationNumber}) {deployedMigration.Name} was expected." ); } } // we'll either take all the migrations after the deployed ones, or we'll take the last migration to restart it plus all the rest. - int indexOfFirstMigrationToRun = migrationsAlreadyRun.Count; + int indexOfFirstMigrationToRun = _deployedMigrations.Count; if (mustRestartLastMigration) indexOfFirstMigrationToRun -= 1; @@ -273,7 +283,7 @@ MigrationPlan Error(string message) { // then we found that there is an offline migration that follows at least one online migration therefore this version is undeployable. return Error( - "Cannot migrate due to incompatible branch. " + + "Cannot deploy due to incompatible branch. " + $"Found offline migration ({availableMigration.MigrationNumber}) {availableMigration.Name} that follows an online migration."); } else @@ -284,33 +294,36 @@ MigrationPlan Error(string message) } // determine which repeatable scripts need to run - var repeatableScriptsAlreadyRun = _journal.GetDeployedStoredCodeDefinitions().ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase); - var availableRepeatableScripts = _migrationsProvider.GatherStoredCodeDefinitions(); + var deployedStoredCodeDefinitions = + _journal.GetDeployedStoredCodeDefinitions().ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase); + + var availableStoredCodeDefinitions = _migrationsProvider.GatherStoredCodeDefinitions(); + // find the differences - storedCodeToRun = availableRepeatableScripts + storedCodeToRun = availableStoredCodeDefinitions .Where(availableScript => - { - DeployedStoredCodeDefinition found; - if (repeatableScriptsAlreadyRun.TryGetValue(availableScript.Name, out found) && found.Fingerprint == availableScript.Fingerprint) { - return false; + if (deployedStoredCodeDefinitions.TryGetValue(availableScript.Name, out var found) + && found.Fingerprint == availableScript.Fingerprint) + { + return false; + } + return true; } - return true; - }) + ) .ToList(); // determine whether online migration is possible var onlineErrorMessage = offlineMigrationsToRun.Count > 1 - ? $"Cannot migrate online due to offline migrations that need to run first: ({offlineMigrationsToRun[0].MigrationNumber}) {offlineMigrationsToRun[0].Name}." + ? $"Cannot deploy online due to offline migrations that need to run first: ({offlineMigrationsToRun[0].MigrationNumber}) {offlineMigrationsToRun[0].Name}." : null; - return new MigrationPlan(null, onlineErrorMessage, offlineMigrationsToRun, storedCodeToRun, onlineMigrationsToRun, lastAlreadyCompletedMigrationNumber); + return new DeploymentPlan(null, onlineErrorMessage, offlineMigrationsToRun, storedCodeToRun, onlineMigrationsToRun, lastCompletedMigrationNumber); } - private IReadOnlyList GetDeployedMigrations() + private void LoadDeployedMigrations() { _deployedMigrations = _deployedMigrations ?? _journal.GetDeployedMigrations(); - return _deployedMigrations; } private IReadOnlyList GetAvailableMigrations() diff --git a/dotMigrator/Properties/AssemblyInfo.cs b/dotMigrator/Properties/AssemblyInfo.cs index aa83c83..bcb163e 100644 --- a/dotMigrator/Properties/AssemblyInfo.cs +++ b/dotMigrator/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.*")] -[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: AssemblyVersion("0.2.*")] +[assembly: AssemblyFileVersion("0.2.0.0")] diff --git a/dotMigrator/StoredCodeDefinition.cs b/dotMigrator/StoredCodeDefinition.cs index 4922cb6..c99c9db 100644 --- a/dotMigrator/StoredCodeDefinition.cs +++ b/dotMigrator/StoredCodeDefinition.cs @@ -2,27 +2,56 @@ namespace dotMigrator { + /// + /// Holds a representation of executable code that is stored in the data store. + /// The Apply method knows how to create or replace that executable code. + /// public class StoredCodeDefinition { - private readonly Action _executeAction; + private readonly Action _applyAction; + /// + /// Constructs this object + /// + /// A case-insensitive name to uniquely identify this definition. Typically a filename. + /// A string that can be used to compare the content between deployments. Typically a cryptographic hash. + /// The delegate that can apply the new definition to the data store. + /// Used to identify the sequence this needs to be applied relative to other stored code it might depend upon. Lower numbers are applied before higher numbers. public StoredCodeDefinition( string name, string fingerprint, - Action executeAction + Action applyAction, + int dependencyLevel ) { - _executeAction = executeAction; + _applyAction = applyAction; Name = name; Fingerprint = fingerprint; + DependencyLevel = dependencyLevel; } + /// + /// Uniquely identifies this definition within the journal associated with this data store + /// public string Name { get; } + + /// + /// Uniquely identifies the content of this stored code definition to determine what has changed between deployments. Should be kept short. Typically a cryptographic hash. + /// public string Fingerprint { get; } - public void Execute(IProgressReporter progressReporter) + /// + /// The level of this definition in a dependency tree. Definitions with a lower dependency level will be applied before those with a higher level. + /// + public int DependencyLevel { get; } + + /// + /// Creates or replaces the stored code definition in the target data store + /// + /// + public void Apply(IProgressReporter progressReporter) { - _executeAction.Invoke(progressReporter); + _applyAction.Invoke(progressReporter); } } } \ No newline at end of file diff --git a/dotMigrator/dotMigrator.csproj b/dotMigrator/dotMigrator.csproj index 06518b9..06f937e 100644 --- a/dotMigrator/dotMigrator.csproj +++ b/dotMigrator/dotMigrator.csproj @@ -36,6 +36,7 @@ + @@ -44,11 +45,13 @@ + - + + \ No newline at end of file