Skip to content

Commit

Permalink
Added Restore from path option for Sql Server only migrations (#136)
Browse files Browse the repository at this point in the history
* Adding options for restore
* Added not implemented exception for remaining ansi databases
* Update docs to mention new configuration option
* Remove not used tokens and uncommented out restore so it can be used
  • Loading branch information
bptillman authored Nov 28, 2021
1 parent ac10fe1 commit 07c7360
Show file tree
Hide file tree
Showing 17 changed files with 168 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/ConfigurationOptions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ grate --connectionstring="Server=(localdb)\MSSQLLocalDB;Integrated Security=true
| --baseline | - | **Baseline** - This instructs grate to mark the scripts as run, but not to actually run anything against the database. Use this option if you already have scripts that have been run through other means (and BEFORE you start the new ones). |
| --forceanytimescripts<br>--runallanytimescripts | false | **RunAllAnyTimeScripts** - This instructs grate to run any time scripts every time it is run even if they haven't changed. Defaults to false.
| --dryrun | false | **DryRun** - This instructs grate to log what would have run, but not to actually run anything against the database. Use this option if you are trying to figure out what grate is going to do. |
| --restore | - | **Restore** - This instructs grate where to find the database backup file (.bak) to restore from. If this option is not specified, no restore will be done.
| -v<br>--verbosity &lt;Critical\|<br>Debug\|<br>Error\|<br>Information\|<br>None\|<br>Trace\|Warning&gt; | Information | **Verbosity level** (as defined here: https://docs.microsoft.com/dotnet/api/Microsoft.Extensions.Logging.LogLevel)
| -?<br>-h<br>--help | - | Show help and usage information7
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Dapper;
using FluentAssertions;
using grate.Configuration;
using grate.unittests.TestInfrastructure;
using NUnit.Framework;

namespace grate.unittests.SqlServer.Running_MigrationScripts
{
[TestFixture]
[Category("SqlServer")]
public class RestoreDatabase : SqlServerScriptsBase
{
protected override IGrateTestContext Context => GrateTestContext.SqlServer;
private readonly string _backupPath = "/var/opt/mssql/backup/test.bak";

[OneTimeSetUp]
public async Task RunBeforeTest()
{
await using (var conn = Context.CreateDbConnection("master"))
{
await conn.ExecuteAsync("use [master] CREATE DATABASE [test]");
await conn.ExecuteAsync("use [test] CREATE TABLE dbo.Table_1 (column1 int NULL)");
await conn.ExecuteAsync($"BACKUP DATABASE [test] TO DISK = '{_backupPath}'");
await conn.ExecuteAsync("use [master] DROP DATABASE [test]");
}
}

[Test]
public async Task Ensure_database_gets_restored()
{
var db = TestConfig.RandomDatabase();

var knownFolders = KnownFolders.In(CreateRandomTempDirectory());
CreateDummySql(knownFolders.Sprocs);

var restoreConfig = Context.GetConfiguration(db, knownFolders) with
{
Restore = _backupPath
};

await using (var migrator = Context.GetMigrator(restoreConfig))
{
await migrator.Migrate();
}

int[] results;
string sql = $"select count(1) from sys.tables where [name]='Table_1'";

await using (var conn = Context.CreateDbConnection(db))
{
results = (await conn.QueryAsync<int>(sql)).ToArray();
}

results.First().Should().Be(1);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using grate.unittests.Generic.Running_MigrationScripts;

namespace grate.unittests.SqlServer.Running_MigrationScripts
{
public abstract class SqlServerScriptsBase : MigrationsScriptsBase
{
}
}
1 change: 0 additions & 1 deletion grate.unittests/grate.unittests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,4 @@
<ItemGroup>
<ProjectReference Include="..\grate\grate.csproj" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions grate/Commands/MigrateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public MigrateCommand(GrateMigrator mi) : base("Migrates the database")
Add(Baseline());
Add(RunAllAnyTimeScripts());
Add(DryRun());
Add(Restore());

Handler = CommandHandler.Create(
async () =>
Expand Down Expand Up @@ -197,5 +198,11 @@ private static Option<bool> DryRun() =>
new[] { "--dryrun" },
" DryRun - This instructs grate to log what would have run, but not to actually run anything against the database. Use this option if you are trying to figure out what grate is going to do."
);

private static Option<string> Restore() =>
new(
new[] { "--restore" },
" Restore - This instructs grate where to get the backed up database file. Defaults to NULL."
);
}
}
5 changes: 5 additions & 0 deletions grate/Configuration/GrateConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,10 @@ public string? AdminConnectionString
/// If true runs all AnyTime scripts even if they haven't changed.
/// </summary>
public bool RunAllAnyTimeScripts { get; init; }

/// <summary>
/// If specified, location of the backup file to use when restoring
/// </summary>
public string? Restore { get; init; }
}
}
4 changes: 1 addition & 3 deletions grate/Infrastructure/TokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ public TokenProvider(GrateConfiguration config, IDatabase db)
["PermissionsFolderName"] = _config.KnownFolders?.Permissions.ToToken(),
//["RecoveryMode"] = RecoveryMode.to_string(),
//["RepositoryPath"] = RepositoryPath.to_string(),
//["Restore"] = Restore.to_string(),
//["RestoreCustomOptions"] = RestoreCustomOptions.to_string(),
//["RestoreFromPath"] = RestoreFromPath.to_string(),
["Restore"] = _config.Restore,
//["RestoreTimeout"] = RestoreTimeout.to_string(),
["RunAfterCreateDatabaseFolderName"] = _config.KnownFolders?.RunAfterCreateDatabase.ToToken(),
["RunAfterOtherAnyTimeScriptsFolderName"] = _config.KnownFolders?.RunAfterOtherAnyTimeScripts.ToToken(),
Expand Down
4 changes: 3 additions & 1 deletion grate/Migration/AnsiSqlDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public virtual async Task<bool> DatabaseExists()
}
}

private async Task WaitUntilDatabaseIsReady()
protected async Task WaitUntilDatabaseIsReady()
{
const int maxDelay = 10_000;
int totalDelay = 0;
Expand Down Expand Up @@ -499,5 +499,7 @@ public async ValueTask DisposeAsync()

GC.SuppressFinalize(this);
}

public abstract Task RestoreDatabase(string backupPath);
}
}
5 changes: 5 additions & 0 deletions grate/Migration/DbMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ async Task<bool> LogAndRunSql()
return theSqlWasRun;
}

public async Task RestoreDatabase(string backupPath)
{
await Database.RestoreDatabase(backupPath);
}

/// <summary>
/// Returns true if we're looking at an AnyTime folder, but the RunAllAnyTimeScripts flag is forced on
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions grate/Migration/GrateMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public async Task Migrate()
databaseCreated = await CreateDatabaseIfItDoesNotExist(dbMigrator);
}

if (!string.IsNullOrEmpty(config.Restore))
{
await RestoreDatabaseFromPath(config.Restore, dbMigrator);
}

TransactionScope? scope = null;
try
{
Expand Down Expand Up @@ -201,6 +206,11 @@ private static async Task<bool> CreateDatabaseIfItDoesNotExist(IDbMigrator dbMig
return databaseCreated;
}

private static async Task RestoreDatabaseFromPath(string backupPath, IDbMigrator dbMigrator)
{
await dbMigrator.RestoreDatabase(backupPath);
}

private async Task LogAndProcess(MigrationsFolder folder, string changeDropFolder, long versionId, ConnectionType connectionType)
{
Separator(' ');
Expand Down
1 change: 1 addition & 0 deletions grate/Migration/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public interface IDatabase : IAsyncDisposable
Task OpenAdminConnection();
Task CloseAdminConnection();
Task CreateDatabase();
Task RestoreDatabase(string backupPath);

/// <summary>
/// Drops the databse if it exists, and does nothing if it doesn't.
Expand Down
2 changes: 2 additions & 0 deletions grate/Migration/IDbMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ public interface IDbMigrator: IAsyncDisposable
Task<bool> RunSql(string sql, string scriptName, MigrationType migrationType, long versionId,
GrateEnvironment? environment,
ConnectionType connectionType);

Task RestoreDatabase(string backupPath);
}
}
6 changes: 6 additions & 0 deletions grate/Migration/MariaDbDatabase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Data.Common;
using System.Threading.Tasks;
using grate.Infrastructure;
using Microsoft.Extensions.Logging;
using MySqlConnector;
Expand All @@ -14,5 +15,10 @@ public MariaDbDatabase(ILogger<MariaDbDatabase> logger)
public override bool SupportsDdlTransactions => false;
public override bool SupportsSchemas => false;
protected override DbConnection GetSqlConnection(string? connectionString) => new MySqlConnection(connectionString);

public override Task RestoreDatabase(string backupPath)
{
throw new System.NotImplementedException("Restoring a database from file is not currently supported for Maria DB.");
}
}
}
7 changes: 6 additions & 1 deletion grate/Migration/OracleDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ protected override async Task CreateScriptsRunErrorsTable()
await CreateIdInsertTrigger(ScriptsRunErrorsTable);
}
}


public override Task RestoreDatabase(string backupPath)
{
throw new System.NotImplementedException("Restoring a database from file is not currently supported for Maria DB.");
}

protected override async Task CreateVersionTable()
{
if (!await VersionTableExists())
Expand Down
6 changes: 6 additions & 0 deletions grate/Migration/PostgreSqlDatabase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Data.Common;
using System.Threading.Tasks;
using grate.Infrastructure;
using Microsoft.Extensions.Logging;
using Npgsql;
Expand All @@ -14,5 +15,10 @@ public PostgreSqlDatabase(ILogger<PostgreSqlDatabase> logger)
public override bool SupportsDdlTransactions => true;
public override bool SupportsSchemas => true;
protected override DbConnection GetSqlConnection(string? connectionString) => new NpgsqlConnection(connectionString);

public override Task RestoreDatabase(string backupPath)
{
throw new System.NotImplementedException("Restoring a database from file is not currently supported for Postgresql.");
}
}
}
5 changes: 5 additions & 0 deletions grate/Migration/SqLiteDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,10 @@ public override Task<bool> DatabaseExists()
}

private static string GetDatabaseName(DbConnection conn) => Path.GetFileNameWithoutExtension(conn.DataSource);

public override Task RestoreDatabase(string backupPath)
{
throw new System.NotImplementedException("Restoring a database from file is not currently supported for SqlLite.");
}
}
}
42 changes: 41 additions & 1 deletion grate/Migration/SqlServerDatabase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Data.Common;
using System;
using System.Data.Common;
using System.Threading.Tasks;
using System.Transactions;
using grate.Infrastructure;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
Expand All @@ -14,5 +17,42 @@ public SqlServerDatabase(ILogger<SqlServerDatabase> logger)
public override bool SupportsDdlTransactions => true;
public override bool SupportsSchemas => true;
protected override DbConnection GetSqlConnection(string? connectionString) => new SqlConnection(connectionString);

public override async Task RestoreDatabase(string backupPath)
{
try
{
await OpenAdminConnection();
Logger.LogInformation("Restoring {dbName} database on {server} server from path {path}.", DatabaseName, ServerName, backupPath);
using var s = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
var cmd = AdminConnection.CreateCommand();
cmd.CommandText =
$@"USE master
ALTER DATABASE [{DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE [{DatabaseName}]
FROM DISK = N'{backupPath}'
WITH NOUNLOAD
, STATS = 10
, RECOVERY
, REPLACE;
ALTER DATABASE [{DatabaseName}] SET MULTI_USER;";
await cmd.ExecuteNonQueryAsync();
s.Complete();
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Got error: " + ex.Message);
throw;
}
finally
{
await CloseAdminConnection();
}

await WaitUntilDatabaseIsReady();

Logger.LogInformation("Database {dbName} successfully restored from path {path}.", DatabaseName, backupPath);
}
}
}

0 comments on commit 07c7360

Please sign in to comment.