From 40becc764a1e1658afcea4a1f76857f305c36e97 Mon Sep 17 00:00:00 2001 From: "Erik A. Brandstadmoen" Date: Tue, 23 Jul 2024 11:23:10 +0200 Subject: [PATCH] Bug #536: Run After Create Database scripts are run even if database 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 --- .../Configuration/FoldersConfiguration.cs | 33 ++--- .../Configuration/KnownFolderKeys.cs | 7 +- .../Configuration/MigrationsFolder.cs | 14 +- src/grate.core/Migration/GrateMigrator.cs | 2 +- .../Run_After_Create_Database_scripts.cs | 10 ++ .../Run_After_Create_Database_scripts.cs | 10 ++ .../Run_After_Create_Database_scripts.cs | 14 ++ .../Run_After_Create_Database_scripts.cs | 10 ++ .../Run_After_Create_Database_scripts.cs | 10 ++ unittests/Sqlite/Database.cs | 38 +---- .../Run_After_Create_Database_scripts.cs | 20 +++ .../TestCommon/Generic/GenericDatabase.cs | 90 +----------- .../Run_After_Create_Database_scripts.cs | 111 +++++++++++++++ unittests/TestCommon/TestCommon.csproj | 2 +- .../TestInfrastructure/DatabaseHelpers.cs | 132 ++++++++++++++++++ 15 files changed, 362 insertions(+), 141 deletions(-) create mode 100644 unittests/MariaDB/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/Oracle/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/PostgreSQL/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/SqlServer/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/SqlServerCaseSensitive/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/Sqlite/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/TestCommon/Generic/Running_MigrationScripts/Run_After_Create_Database_scripts.cs create mode 100644 unittests/TestCommon/TestInfrastructure/DatabaseHelpers.cs diff --git a/src/grate.core/Configuration/FoldersConfiguration.cs b/src/grate.core/Configuration/FoldersConfiguration.cs index 598427a8..7ad19b60 100644 --- a/src/grate.core/Configuration/FoldersConfiguration.cs +++ b/src/grate.core/Configuration/FoldersConfiguration.cs @@ -1,4 +1,5 @@ using grate.Migration; +using static grate.Configuration.KnownFolderKeys; using static grate.Configuration.MigrationType; namespace grate.Configuration; @@ -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; } diff --git a/src/grate.core/Configuration/KnownFolderKeys.cs b/src/grate.core/Configuration/KnownFolderKeys.cs index 260cc0ba..698046de 100644 --- a/src/grate.core/Configuration/KnownFolderKeys.cs +++ b/src/grate.core/Configuration/KnownFolderKeys.cs @@ -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 Keys = new[] - { + public static readonly IEnumerable Keys = + [ CreateDatabase, BeforeMigration, AlterDatabase, RunAfterCreateDatabase, RunBeforeUp, Up, RunFirstAfterUp, Functions, Views, Sprocs, Triggers, Indexes, RunAfterOtherAnyTimeScripts, Permissions, AfterMigration - }; + ]; } diff --git a/src/grate.core/Configuration/MigrationsFolder.cs b/src/grate.core/Configuration/MigrationsFolder.cs index 1821377e..deb52cfb 100644 --- a/src/grate.core/Configuration/MigrationsFolder.cs +++ b/src/grate.core/Configuration/MigrationsFolder.cs @@ -13,6 +13,7 @@ namespace grate.Configuration; /// 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. public record MigrationsFolder( + string Key, string Name, string Path, MigrationType Type = MigrationType.Once, @@ -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}"; } diff --git a/src/grate.core/Migration/GrateMigrator.cs b/src/grate.core/Migration/GrateMigrator.cs index acccddfc..9885bb96 100644 --- a/src/grate.core/Migration/GrateMigrator.cs +++ b/src/grate.core/Migration/GrateMigrator.cs @@ -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; } diff --git a/unittests/MariaDB/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/MariaDB/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..cb458085 --- /dev/null +++ b/unittests/MariaDB/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -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); diff --git a/unittests/Oracle/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/Oracle/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..fdf28f75 --- /dev/null +++ b/unittests/Oracle/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -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); diff --git a/unittests/PostgreSQL/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/PostgreSQL/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..c4dae3e1 --- /dev/null +++ b/unittests/PostgreSQL/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -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); diff --git a/unittests/SqlServer/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/SqlServer/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..d7955492 --- /dev/null +++ b/unittests/SqlServer/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -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); diff --git a/unittests/SqlServerCaseSensitive/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/SqlServerCaseSensitive/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..89fc9587 --- /dev/null +++ b/unittests/SqlServerCaseSensitive/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -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); diff --git a/unittests/Sqlite/Database.cs b/unittests/Sqlite/Database.cs index d2092dbb..cb1aaa98 100644 --- a/unittests/Sqlite/Database.cs +++ b/unittests/Sqlite/Database.cs @@ -1,6 +1,6 @@ -using Microsoft.Data.Sqlite; -using Sqlite.TestInfrastructure; +using Sqlite.TestInfrastructure; using TestCommon.TestInfrastructure; +using static TestCommon.TestInfrastructure.DatabaseHelpers; namespace Sqlite; @@ -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> GetDatabases() - { - var builder = new SqliteConnectionStringBuilder(this.Context.AdminConnectionString); - var root = Path.GetDirectoryName(builder.DataSource) ?? Directory.CreateTempSubdirectory().ToString() ; - var dbFiles = Directory.EnumerateFiles(root, "*.db"); - IEnumerable dbNames = dbFiles - .Select(Path.GetFileNameWithoutExtension) - .Where(name => name is not null) - .Cast(); - return await ValueTask.FromResult(dbNames); - } + protected override Task CreateDatabaseFromConnectionString(string db, string connectionString) + => CreateSqliteDatabaseFromConnectionString(connectionString); + protected override Task> 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; diff --git a/unittests/Sqlite/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/Sqlite/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..e0cb0629 --- /dev/null +++ b/unittests/Sqlite/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -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> GetDatabases() => Context.GetSqliteDatabases(); + + protected override Task CreateDatabaseFromConnectionString(string db, string connectionString) + => CreateSqliteDatabaseFromConnectionString(connectionString); +} diff --git a/unittests/TestCommon/Generic/GenericDatabase.cs b/unittests/TestCommon/Generic/GenericDatabase.cs index 2fbabb71..cf2737c7 100644 --- a/unittests/TestCommon/Generic/GenericDatabase.cs +++ b/unittests/TestCommon/Generic/GenericDatabase.cs @@ -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> GetDatabases() - { - IEnumerable databases = Enumerable.Empty(); - 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(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> GetDatabases() => Context.GetDatabases(TestOutput); protected virtual bool ThrowOnMissingDatabase => true; diff --git a/unittests/TestCommon/Generic/Running_MigrationScripts/Run_After_Create_Database_scripts.cs b/unittests/TestCommon/Generic/Running_MigrationScripts/Run_After_Create_Database_scripts.cs new file mode 100644 index 00000000..dd9d0e0d --- /dev/null +++ b/unittests/TestCommon/Generic/Running_MigrationScripts/Run_After_Create_Database_scripts.cs @@ -0,0 +1,111 @@ +using Dapper; +using FluentAssertions; +using FluentAssertions.Execution; +using grate.Configuration; +using TestCommon.TestInfrastructure; +using Xunit.Abstractions; +using Xunit.Sdk; +using static grate.Configuration.KnownFolderKeys; + +namespace TestCommon.Generic.Running_MigrationScripts; + +// ReSharper disable once InconsistentNaming +public abstract class Run_After_Create_Database_scripts(IGrateTestContext context, ITestOutputHelper testOutput) + : MigrationsScriptsBase(context, testOutput) +{ + [Fact] + public virtual async Task Are_run_if_the_database_is_created_from_scratch() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Sprocs], "1_sprocs.sql"); + WriteSomeOtherSql(parent, knownFolders[RunAfterCreateDatabase], "1_runAfterCreateDatabase.sql"); + + // Do NOT create the database manually before running the migration + + // Check that the database does not exist + IEnumerable databasesBeforeMigration = await GetDatabases(); + databasesBeforeMigration.Should().NotContain(db); + + // Run the migration + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + // Check that the "Run after create database" scripts have been run + string[] scripts; + string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.External.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(2); + + using (new AssertionScope()) + { + scripts.First().Should().Be("1_runAfterCreateDatabase.sql"); + scripts.Last().Should().Be("1_sprocs.sql"); + } + } + + [Fact] + public async Task Are_not_run_if_the_database_is_not_created_from_scratch() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = Folders.Default; + CreateDummySql(parent, knownFolders[Sprocs], "1_sprocs.sql"); + WriteSomeOtherSql(parent, knownFolders[RunAfterCreateDatabase], "1_runAfterCreateDatabase.sql"); + + // Create the database manually before running the migration + await CreateDatabaseFromConnectionString(db, Context.UserConnectionString(db)); + + // Check that the database has been created + IEnumerable databasesBeforeMigration = await GetDatabases(); + databasesBeforeMigration.Should().Contain(db); + + // Run the migration + var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration) + .WithConnectionString(Context.ConnectionString(db)) + .WithFolders(knownFolders) + .WithSqlFilesDirectory(parent) + .Build(); + + await using (var migrator = Context.Migrator.WithConfiguration(config)) + { + await migrator.Migrate(); + } + + // Check that the "Run after create database" scripts have not been run + string[] scripts; + string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + using (var conn = Context.External.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.Should().HaveCount(1); + using (new AssertionScope()) + { + scripts.Single().Should().Be("1_sprocs.sql"); + } + } + + protected virtual Task> GetDatabases() => Context.GetDatabases(TestOutput); + + protected virtual Task CreateDatabaseFromConnectionString(string db, string connectionString) + => Context.CreateDatabaseFromConnectionString(db, connectionString, TestOutput); +} diff --git a/unittests/TestCommon/TestCommon.csproj b/unittests/TestCommon/TestCommon.csproj index ca3f75f3..971298d1 100644 --- a/unittests/TestCommon/TestCommon.csproj +++ b/unittests/TestCommon/TestCommon.csproj @@ -7,7 +7,7 @@ false true false - IL2072;IL2075;IL2026 + IL2072;IL2075;IL2026 diff --git a/unittests/TestCommon/TestInfrastructure/DatabaseHelpers.cs b/unittests/TestCommon/TestInfrastructure/DatabaseHelpers.cs new file mode 100644 index 00000000..bc305fd4 --- /dev/null +++ b/unittests/TestCommon/TestInfrastructure/DatabaseHelpers.cs @@ -0,0 +1,132 @@ +using System.Data.Common; +using System.Transactions; +using Dapper; +using Microsoft.Data.Sqlite; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestCommon.TestInfrastructure; + +public static class DatabaseHelpers +{ + internal static async Task CreateDatabaseFromConnectionString(this IGrateTestContext context, string db, string connectionString, ITestOutputHelper output) + { + 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) + { + output.WriteLine("Got error when creating database: " + dbe.Message); + output.WriteLine("database: " + db); + output.WriteLine("admin connection string: " + conn.ConnectionString); + output.WriteLine("user connection string: " + connectionString); + output.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) + { + output.WriteLine("Got error when creating user: " + dbe.Message); + output.WriteLine("Error creating user: " + uid + " for database: " + db); + output.WriteLine("admin connection string: " + conn.ConnectionString); + output.WriteLine("user connection string: " + connectionString); + output.WriteLine("createUserSql: " + createUserSql); + } + + var grantAccessSql = context.Sql.GrantAccess(db, uid); + if (grantAccessSql is not null) + { + await conn.ExecuteAsync(grantAccessSql); + } + + break; + } + catch (DbException dbe) + { + output.WriteLine($"Got error in loop, iteration: {i}: {dbe.Message}"); + } + + await Task.Delay(1000); + } + } + } + + internal static async Task> GetDatabases(this IGrateTestContext context, ITestOutputHelper output) + { + IEnumerable databases = Enumerable.Empty(); + 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(sql)).ToArray(); + break; + } + catch (DbException dbe) + { + output.WriteLine("Got error when listing databases: " + dbe.Message); + output.WriteLine("admin connection string: " + conn.ConnectionString); + } + } + } + return databases.ToArray(); + } + + public static async Task CreateSqliteDatabaseFromConnectionString(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(); + } + + public static async Task> GetSqliteDatabases(this IGrateTestContext context) + { + var builder = new SqliteConnectionStringBuilder(context.AdminConnectionString); + var root = Path.GetDirectoryName(builder.DataSource) ?? Directory.CreateTempSubdirectory().ToString() ; + var dbFiles = Directory.EnumerateFiles(root, "*.db"); + IEnumerable dbNames = dbFiles + .Select(Path.GetFileNameWithoutExtension) + .Where(name => name is not null) + .Cast(); + + return await ValueTask.FromResult(dbNames); + } + + +}