Skip to content

Commit

Permalink
Bug #536: Run After Create Database scripts are run even if database …
Browse files Browse the repository at this point in the history
…isn't created from scratch by grate

* Wrote tests demonstrating the problem
* Introduced "Key" in MigrationsFolder
* Check on this to see if the folder is the "RunAfterCreateDatabase" folder
  • Loading branch information
erikbra committed Jul 23, 2024
1 parent c5a1740 commit c44c0a8
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 141 deletions.
33 changes: 17 additions & 16 deletions src/grate.core/Configuration/FoldersConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using grate.Migration;
using static grate.Configuration.KnownFolderKeys;
using static grate.Configuration.MigrationType;

namespace grate.Configuration;
Expand Down Expand Up @@ -35,23 +36,23 @@ public static IFoldersConfiguration Default(IKnownFolderNames? folderNames = nul

var foldersConfiguration = new FoldersConfiguration()
{
{ KnownFolderKeys.BeforeMigration, new MigrationsFolder("BeforeMigration", folderNames.BeforeMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) },
{ KnownFolderKeys.AlterDatabase , new MigrationsFolder("AlterDatabase", folderNames.AlterDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous) },
{ KnownFolderKeys.RunAfterCreateDatabase, new MigrationsFolder("Run After Create Database", folderNames.RunAfterCreateDatabase, AnyTime) },
{ KnownFolderKeys.RunBeforeUp, new MigrationsFolder("Run Before Update", folderNames.RunBeforeUp, AnyTime) },
{ KnownFolderKeys.Up, new MigrationsFolder("Update", folderNames.Up, Once) },
{ KnownFolderKeys.RunFirstAfterUp, new MigrationsFolder("Run First After Update", folderNames.RunFirstAfterUp, AnyTime) },
{ KnownFolderKeys.Functions, new MigrationsFolder("Functions", folderNames.Functions, AnyTime) },
{ KnownFolderKeys.Views, new MigrationsFolder("Views", folderNames.Views, AnyTime) },
{ KnownFolderKeys.Sprocs, new MigrationsFolder("Stored Procedures", folderNames.Sprocs, AnyTime) },
{ KnownFolderKeys.Triggers, new MigrationsFolder("Triggers", folderNames.Triggers, AnyTime) },
{ KnownFolderKeys.Indexes, new MigrationsFolder("Indexes", folderNames.Indexes, AnyTime) },
{ KnownFolderKeys.RunAfterOtherAnyTimeScripts, new MigrationsFolder("Run after Other Anytime Scripts", folderNames.RunAfterOtherAnyTimeScripts, AnyTime) },
{ KnownFolderKeys.Permissions, new MigrationsFolder("Permissions", folderNames.Permissions, EveryTime, TransactionHandling: TransactionHandling.Autonomous) },
{ KnownFolderKeys.AfterMigration, new MigrationsFolder("AfterMigration", folderNames.AfterMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) },
{ BeforeMigration, new MigrationsFolder(BeforeMigration, "BeforeMigration", folderNames.BeforeMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) },
{ AlterDatabase , new MigrationsFolder(AlterDatabase, "AlterDatabase", folderNames.AlterDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous) },
{ RunAfterCreateDatabase, new MigrationsFolder(RunAfterCreateDatabase, "Run After Create Database", folderNames.RunAfterCreateDatabase, AnyTime) },
{ RunBeforeUp, new MigrationsFolder(RunBeforeUp, "Run Before Update", folderNames.RunBeforeUp, AnyTime) },
{ Up, new MigrationsFolder(Up, "Update", folderNames.Up, Once) },
{ RunFirstAfterUp, new MigrationsFolder(RunFirstAfterUp, "Run First After Update", folderNames.RunFirstAfterUp, AnyTime) },
{ Functions, new MigrationsFolder(Functions, "Functions", folderNames.Functions, AnyTime) },
{ Views, new MigrationsFolder(Views, "Views", folderNames.Views, AnyTime) },
{ Sprocs, new MigrationsFolder(Sprocs, "Stored Procedures", folderNames.Sprocs, AnyTime) },
{ Triggers, new MigrationsFolder(Triggers, "Triggers", folderNames.Triggers, AnyTime) },
{ Indexes, new MigrationsFolder(Indexes, "Indexes", folderNames.Indexes, AnyTime) },
{ RunAfterOtherAnyTimeScripts, new MigrationsFolder(RunAfterOtherAnyTimeScripts, "Run after Other Anytime Scripts", folderNames.RunAfterOtherAnyTimeScripts, AnyTime) },
{ Permissions, new MigrationsFolder(Permissions, "Permissions", folderNames.Permissions, EveryTime, TransactionHandling: TransactionHandling.Autonomous) },
{ AfterMigration, new MigrationsFolder(AfterMigration, "AfterMigration", folderNames.AfterMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) },
};
foldersConfiguration.CreateDatabase = new MigrationsFolder("CreateDatabase", folderNames.CreateDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous);
foldersConfiguration.DropDatabase = new MigrationsFolder("DropDatabase", folderNames.DropDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous);
foldersConfiguration.CreateDatabase = new MigrationsFolder(KnownFolderKeys.CreateDatabase, "CreateDatabase", folderNames.CreateDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous);
foldersConfiguration.DropDatabase = new MigrationsFolder(KnownFolderKeys.DropDatabase, "DropDatabase", folderNames.DropDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous);

return foldersConfiguration;
}
Expand Down
7 changes: 4 additions & 3 deletions src/grate.core/Configuration/KnownFolderKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ internal static class KnownFolderKeys
public const string RunAfterOtherAnyTimeScripts = nameof(RunAfterOtherAnyTimeScripts);
public const string Permissions = nameof(Permissions);
public const string AfterMigration = nameof(AfterMigration);
public const string DropDatabase = nameof(DropDatabase);

public static readonly IEnumerable<string> Keys = new[]
{
public static readonly IEnumerable<string> Keys =
[
CreateDatabase, BeforeMigration, AlterDatabase, RunAfterCreateDatabase, RunBeforeUp, Up, RunFirstAfterUp, Functions, Views,
Sprocs, Triggers, Indexes, RunAfterOtherAnyTimeScripts, Permissions, AfterMigration
};
];
}
14 changes: 12 additions & 2 deletions src/grate.core/Configuration/MigrationsFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace grate.Configuration;
/// <param name="TransactionHandling">Whether to roll back this folder if something fails, or run these
/// scripts in a separate, autonomous transactions, which makes them run no matter if other stuff errors.</param>
public record MigrationsFolder(
string Key,
string Name,
string Path,
MigrationType Type = MigrationType.Once,
Expand All @@ -21,12 +22,21 @@ public record MigrationsFolder(
{
public MigrationsFolder(
string name,
string path,
MigrationType type = MigrationType.Once,
ConnectionType connectionType = ConnectionType.Default,
TransactionHandling transactionHandling = TransactionHandling.Default)
: this(name, name, type, connectionType, transactionHandling)
: this(name, name, path, type, connectionType, transactionHandling)
{ }

public MigrationsFolder(
string name,
MigrationType type = MigrationType.Once,
ConnectionType connectionType = ConnectionType.Default,
TransactionHandling transactionHandling = TransactionHandling.Default)
: this(name, name, name, type, connectionType, transactionHandling)
{ }

public override string ToString() =>
$"{Name}=path:{Path},type:{Type},connectionType:{ConnectionType},transactionHandling:{TransactionHandling}";
$"{Key}=name:{Name},path:{Path},type:{Type},connectionType:{ConnectionType},transactionHandling:{TransactionHandling}";
}
2 changes: 1 addition & 1 deletion src/grate.core/Migration/GrateMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public async Task Migrate()

// This is an ugly "if" run on every script, to check one special folder which has conditions.
// If possible, we should find a 'cleaner' way to do this.
if (nameof(KnownFolderNames.RunAfterCreateDatabase).Equals(folder?.Name) && !databaseCreated)
if (KnownFolderKeys.RunAfterCreateDatabase.Equals(folder?.Key) && !databaseCreated)
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using MariaDB.TestInfrastructure;
using TestCommon.TestInfrastructure;

namespace MariaDB.Running_MigrationScripts;

[Collection(nameof(MariaDbGrateTestContext))]
// ReSharper disable once InconsistentNaming
// ReSharper disable once UnusedType.Global
public class Run_After_Create_Database_scripts(MariaDbGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.Running_MigrationScripts.Run_After_Create_Database_scripts(testContext, testOutput);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Oracle.TestInfrastructure;
using TestCommon.TestInfrastructure;

namespace Oracle.Running_MigrationScripts;

[Collection(nameof(OracleGrateTestContext))]
// ReSharper disable once InconsistentNaming
// ReSharper disable once UnusedType.Global
public class Run_After_Create_Database_scripts(OracleGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.Running_MigrationScripts.Run_After_Create_Database_scripts(testContext, testOutput);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Dapper;
using FluentAssertions;
using grate.Configuration;
using PostgreSQL.TestInfrastructure;
using TestCommon.TestInfrastructure;
using static grate.Configuration.KnownFolderKeys;

namespace PostgreSQL.Running_MigrationScripts;

[Collection(nameof(PostgreSqlGrateTestContext))]
// ReSharper disable once InconsistentNaming
// ReSharper disable once UnusedType.Global
public class Run_After_Create_Database_scripts(PostgreSqlGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.Running_MigrationScripts.Run_After_Create_Database_scripts(testContext, testOutput);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SqlServer.TestInfrastructure;
using TestCommon.TestInfrastructure;

namespace SqlServer.Running_MigrationScripts;

[Collection(nameof(SqlServerGrateTestContext))]
// ReSharper disable once InconsistentNaming
// ReSharper disable once UnusedType.Global
public class Run_After_Create_Database_scripts(SqlServerGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.Running_MigrationScripts.Run_After_Create_Database_scripts(testContext, testOutput);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SqlServerCaseSensitive.TestInfrastructure;
using TestCommon.TestInfrastructure;

namespace SqlServerCaseSensitive.Running_MigrationScripts;

[Collection(nameof(SqlServerGrateTestContext))]
// ReSharper disable once InconsistentNaming
// ReSharper disable once UnusedType.Global
public class Run_After_Create_Database_scripts(SqlServerGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.Running_MigrationScripts.Run_After_Create_Database_scripts(testContext, testOutput);
38 changes: 7 additions & 31 deletions unittests/Sqlite/Database.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.Data.Sqlite;
using Sqlite.TestInfrastructure;
using Sqlite.TestInfrastructure;
using TestCommon.TestInfrastructure;
using static TestCommon.TestInfrastructure.DatabaseHelpers;

namespace Sqlite;

Expand All @@ -10,37 +10,13 @@ namespace Sqlite;
public class Database(SqliteGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.GenericDatabase(testContext, testOutput)
{

protected override async Task CreateDatabaseFromConnectionString(string db, string connectionString)
{
await using var conn = new SqliteConnection(connectionString);
conn.Open();
await using var cmd = conn.CreateCommand();

// Create a table to actually create the .sqlite file
var sql = "CREATE TABLE dummy(name VARCHAR(1))";
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();

// Remove the table to avoid polluting the database with dummy tables :)
sql = "DROP TABLE dummy";
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
}

protected override async Task<IEnumerable<string>> GetDatabases()
{
var builder = new SqliteConnectionStringBuilder(this.Context.AdminConnectionString);
var root = Path.GetDirectoryName(builder.DataSource) ?? Directory.CreateTempSubdirectory().ToString() ;
var dbFiles = Directory.EnumerateFiles(root, "*.db");
IEnumerable<string> dbNames = dbFiles
.Select(Path.GetFileNameWithoutExtension)
.Where(name => name is not null)
.Cast<string>();

return await ValueTask.FromResult(dbNames);
}
protected override Task CreateDatabaseFromConnectionString(string db, string connectionString)
=> CreateSqliteDatabaseFromConnectionString(connectionString);

protected override Task<IEnumerable<string>> GetDatabases()
=> Context.GetSqliteDatabases();

[Fact(Skip = "SQLite does not support custom database creation script")]
public override Task Is_created_with_custom_script_if_custom_create_database_folder_exists() =>
Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Sqlite.TestInfrastructure;
using TestCommon.TestInfrastructure;
using static TestCommon.TestInfrastructure.DatabaseHelpers;

namespace Sqlite.Running_MigrationScripts;

[Collection(nameof(SqliteTestDatabase))]
// ReSharper disable once InconsistentNaming
// ReSharper disable once UnusedType.Global
public class Run_After_Create_Database_scripts(SqliteGrateTestContext testContext, ITestOutputHelper testOutput)
: TestCommon.Generic.Running_MigrationScripts.Run_After_Create_Database_scripts(testContext, testOutput)
{
[Fact(Skip = "Sqlite does not support creating databases using grate")]
public override Task Are_run_if_the_database_is_created_from_scratch() => Task.CompletedTask;

protected override Task<IEnumerable<string>> GetDatabases() => Context.GetSqliteDatabases();

protected override Task CreateDatabaseFromConnectionString(string db, string connectionString)
=> CreateSqliteDatabaseFromConnectionString(connectionString);
}
90 changes: 3 additions & 87 deletions unittests/TestCommon/Generic/GenericDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,94 +157,10 @@ public async Task Does_not_need_admin_connection_if_database_already_exists(stri

protected Task CreateDatabase(string db) => CreateDatabaseFromConnectionString(db, Context.ConnectionString(db));

protected virtual async Task CreateDatabaseFromConnectionString(string db, string connectionString)
{
var uid = TestConfig.Username(connectionString);
var pwd = TestConfig.Password(connectionString);

using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
{
for (var i = 0; i < 5; i++)
{
try
{
using var conn = Context.CreateAdminDbConnection();

string? commandText = null;
try
{
commandText = Context.Syntax.CreateDatabase(db, pwd);
await conn.ExecuteAsync(commandText);
}
catch (DbException dbe)
{
TestOutput.WriteLine("Got error when creating database: " + dbe.Message);
TestOutput.WriteLine("database: " + db);
TestOutput.WriteLine("admin connection string: " + conn.ConnectionString);
TestOutput.WriteLine("user connection string: " + connectionString);
TestOutput.WriteLine("commandText: " + commandText);
}

string? createUserSql = null;
try
{
createUserSql = Context.Sql.CreateUser(db, uid, pwd);
if (createUserSql is not null)
{
await conn.ExecuteAsync(createUserSql);
}
}
catch (DbException dbe)
{
TestOutput.WriteLine("Got error when creating user: " + dbe.Message);
TestOutput.WriteLine("Error creating user: " + uid + " for database: " + db);
TestOutput.WriteLine("admin connection string: " + conn.ConnectionString);
TestOutput.WriteLine("user connection string: " + connectionString);
TestOutput.WriteLine("createUserSql: " + createUserSql);
}

var grantAccessSql = Context.Sql.GrantAccess(db, uid);
if (grantAccessSql is not null)
{
await conn.ExecuteAsync(grantAccessSql);
}

break;
}
catch (DbException dbe)
{
TestOutput.WriteLine($"Got error in loop, iteration: {i}: {dbe.Message}");
}

await Task.Delay(1000);
}
}
}
protected virtual Task CreateDatabaseFromConnectionString(string db, string connectionString) =>
Context.CreateDatabaseFromConnectionString(db, connectionString, TestOutput);

protected virtual async Task<IEnumerable<string>> GetDatabases()
{
IEnumerable<string> databases = Enumerable.Empty<string>();
string sql = Context.Syntax.ListDatabases;

using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
{
for (var i = 0; i < 5; i++)
{
using var conn = Context.CreateAdminDbConnection();
try
{
databases = (await conn.QueryAsync<string>(sql)).ToArray();
break;
}
catch (DbException dbe)
{
TestOutput.WriteLine("Got error when listing databases: " + dbe.Message);
TestOutput.WriteLine("admin connection string: " + conn.ConnectionString);
}
}
}
return databases.ToArray();
}
protected virtual Task<IEnumerable<string>> GetDatabases() => Context.GetDatabases(TestOutput);

protected virtual bool ThrowOnMissingDatabase => true;

Expand Down
Loading

0 comments on commit c44c0a8

Please sign in to comment.