using System.Data;
using System.Reflection;
using DbUp;
using DbUp.Engine;
using Infrastructure.Sql;
using Microsoft.Data.SqlClient;
namespace Database.Migrations;
///
/// Entry point for the database migration runner. Reads connection details from environment
/// variables, optionally drops and recreates the target database, and applies all SQL
/// migration scripts embedded in this assembly using DbUp.
///
public static class Program
{
///
/// Migration runner entry point. Drops the target database when CLEAR_DATABASE is
/// set to true, ensures the database exists, then deploys all pending migrations.
///
///
/// 0 if migrations completed successfully; 1 if they failed or an error occurred.
///
public static async Task Main()
{
Console.WriteLine("Starting database migrations...");
try
{
DatabaseSettings settings = DatabaseSettings.FromEnvironment();
if (settings.ClearDatabase)
{
Console.WriteLine(
$"CLEAR_DATABASE is enabled. Dropping database '{settings.DatabaseName}'..."
);
await DropDatabaseIfExistsAsync(settings);
}
await CreateDatabaseIfNotExistsAsync(settings);
return DeployMigrations(settings.ConnectionString) ? 0 : 1;
}
catch (Exception ex) when (ex is InvalidOperationException or SqlException)
{
Console.Error.WriteLine("An error occurred during database migrations:");
Console.Error.WriteLine(ex.Message);
return 1;
}
}
///
/// Drops the target database if it exists, first forcing it into single-user mode with
/// rollback immediate so that any existing connections are terminated.
///
/// The resolved database settings.
/// Thrown if the connection or the drop operation fails.
private static async Task DropDatabaseIfExistsAsync(DatabaseSettings settings)
{
const string sql = """
DECLARE @statement nvarchar(max);
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
BEGIN
SET @statement =
N'ALTER DATABASE ' + QUOTENAME(@databaseName)
+ N' SET SINGLE_USER WITH ROLLBACK IMMEDIATE;';
EXEC sys.sp_executesql @statement;
SET @statement = N'DROP DATABASE ' + QUOTENAME(@databaseName) + N';';
EXEC sys.sp_executesql @statement;
END
""";
await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
Console.WriteLine($"Database '{settings.DatabaseName}' dropped, or did not exist.");
}
///
/// Creates the target database on the master connection if it does not already exist.
///
/// The resolved database settings.
/// Thrown if the connection or the create operation fails.
private static async Task CreateDatabaseIfNotExistsAsync(DatabaseSettings settings)
{
const string sql = """
DECLARE @statement nvarchar(max);
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
BEGIN
SET @statement = N'CREATE DATABASE ' + QUOTENAME(@databaseName) + N';';
EXEC sys.sp_executesql @statement;
END
""";
await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
Console.WriteLine($"Database '{settings.DatabaseName}' is present.");
}
/// The value bound to the @databaseName parameter.
private static async Task ExecuteNonQueryAsync(
string connectionString,
string sql,
string databaseName
)
{
await using SqlConnection connection = new(connectionString);
await connection.OpenAsync();
await using SqlCommand command = new(sql, connection);
command.Parameters.Add("@databaseName", SqlDbType.NVarChar, 128).Value = databaseName;
await command.ExecuteNonQueryAsync();
}
///
/// Applies all pending SQL migration scripts embedded in this assembly to the target
/// database using DbUp, logging progress to the console.
///
/// The connection string for the target application database.
/// true if the upgrade completed successfully; otherwise false.
private static bool DeployMigrations(string connectionString)
{
UpgradeEngine upgrader = DeployChanges
.To.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.WithTransactionPerScript()
.LogToConsole()
.Build();
DatabaseUpgradeResult result = upgrader.PerformUpgrade();
if (result.Successful)
{
Console.WriteLine("Database migrations completed successfully.");
return true;
}
Console.Error.WriteLine($"Database migrations failed: {result.Error}");
return false;
}
///
/// Connection details and runtime options resolved from environment variables.
///
/// The connection string for the target application database.
///
/// The connection string for the master database, used for create and drop operations.
///
/// The name of the target application database.
/// Whether the target database should be dropped before migrating.
private sealed record DatabaseSettings(
string ConnectionString,
string MasterConnectionString,
string DatabaseName,
bool ClearDatabase
)
{
///
/// Builds the settings from the DB_SERVER, DB_NAME, DB_USER,
/// DB_PASSWORD, DB_TRUST_SERVER_CERTIFICATE, and CLEAR_DATABASE
/// environment variables.
///
///
/// Thrown when a required environment variable is not set.
///
public static DatabaseSettings FromEnvironment()
{
string databaseName =
Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException(
"The DB_NAME environment variable is not set."
);
bool clearDatabase = string.Equals(
Environment.GetEnvironmentVariable("CLEAR_DATABASE"),
"true",
StringComparison.OrdinalIgnoreCase
);
return new DatabaseSettings(
SqlConnectionStringHelper.BuildConnectionString(databaseName),
SqlConnectionStringHelper.BuildMasterConnectionString(),
databaseName,
clearDatabase
);
}
}
}