diff --git a/grate.unittests/Generic/GenericDatabase.cs b/grate.unittests/Generic/GenericDatabase.cs index 1ab33690..bec9a648 100644 --- a/grate.unittests/Generic/GenericDatabase.cs +++ b/grate.unittests/Generic/GenericDatabase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Threading.Tasks; @@ -26,7 +27,7 @@ public async Task Is_created_if_confed_and_it_does_not_exist() await migrator.Migrate(); IEnumerable databases = await GetDatabases(); - databases.Should().Contain(db); + Exist(databases, db).Should().BeTrue(); } [Test] @@ -35,7 +36,7 @@ public async Task Is_not_created_if_not_confed() var db = "SOMEOTHERDATABASE"; IEnumerable databasesBeforeMigration = await GetDatabases(); - databasesBeforeMigration.Should().NotContain(db); + Exist(databasesBeforeMigration, db).Should().BeFalse(); await using var migrator = GetMigrator(GetConfiguration(db, false)); @@ -47,7 +48,7 @@ public async Task Is_not_created_if_not_confed() // Ensure that the database was in fact not created IEnumerable databases = await GetDatabases(); - databases.Should().NotContain(db); + Exist(databases, db).Should().BeFalse(); } [Test] @@ -60,14 +61,14 @@ public async Task Does_not_error_if_confed_to_create_but_already_exists() // Check that the database has been created IEnumerable databasesBeforeMigration = await GetDatabases(); - databasesBeforeMigration.Should().Contain(db); + Exist(databasesBeforeMigration, db).Should().BeTrue(); await using var migrator = GetMigrator(GetConfiguration(db, true)); // There should be no errors running the migration Assert.DoesNotThrowAsync(() => migrator.Migrate()); } - + [Test] public async Task Does_not_need_admin_connection_if_database_already_exists() { @@ -78,7 +79,7 @@ public async Task Does_not_need_admin_connection_if_database_already_exists() // Check that the database has been created IEnumerable databasesBeforeMigration = await GetDatabases(); - databasesBeforeMigration.Should().Contain(db); + Exist(databasesBeforeMigration, db).Should().BeTrue(); // Change the admin connection string to rubbish and run the migration await using var migrator = GetMigrator(GetConfiguration(db, true, "Invalid stuff")); @@ -98,13 +99,36 @@ public async Task Does_not_needlessly_apply_case_sensitive_database_name_checks_ // Check that the database has been created IEnumerable databasesBeforeMigration = await GetDatabases(); - databasesBeforeMigration.Should().Contain(db); + Exist(databasesBeforeMigration, db).Should().BeTrue(); await using var migrator = GetMigrator(GetConfiguration(db.ToLower(), true)); // ToLower is important here, this reproduces the bug in #167 // There should be no errors running the migration Assert.DoesNotThrowAsync(() => migrator.Migrate()); } + [Test] + public async Task Should_create_the_databases_with_name_as_case_is_supported() + { + const string db = "CaseSensitiveName"; + var lowercaseDbName = db.ToLower(); + + await CreateDatabase(db); + await CreateDatabase(lowercaseDbName); + + var databases = (await GetDatabases()).ToArray(); + + if (Context.DatabaseNamingCaseSensitive) + { + databases.Should().Contain(db); + databases.Should().Contain(lowercaseDbName); + } + else + { + var matches = databases.Count(x => string.Equals(x, db, StringComparison.OrdinalIgnoreCase)); + matches.Should().Be(1); + } + } + protected virtual async Task CreateDatabase(string db) { using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) @@ -127,7 +151,7 @@ protected virtual async Task CreateDatabase(string db) protected virtual async Task> GetDatabases() { - IEnumerable databases =Enumerable.Empty(); + IEnumerable databases = Enumerable.Empty(); string sql = Context.Syntax.ListDatabases; using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) @@ -146,6 +170,15 @@ protected virtual async Task> GetDatabases() return databases; } + private bool Exist(IEnumerable databases, string db) + { + var comparer = Context.DatabaseNamingCaseSensitive + ? StringComparer.InvariantCulture + : StringComparer.InvariantCultureIgnoreCase; + + return databases.Contains(db, comparer); + } + protected virtual bool ThrowOnMissingDatabase => true; diff --git a/grate.unittests/MariaDB/Database.cs b/grate.unittests/MariaDB/Database.cs index 959e2275..cd16b639 100644 --- a/grate.unittests/MariaDB/Database.cs +++ b/grate.unittests/MariaDB/Database.cs @@ -1,12 +1,21 @@ -using grate.unittests.TestInfrastructure; +using grate.unittests.TestInfrastructure; using NUnit.Framework; namespace grate.unittests.MariaDB; -[TestFixture] -[Category("MariaDB")] -public class Database: Generic.GenericDatabase +public class Database { - protected override IGrateTestContext Context => GrateTestContext.MariaDB; - -} \ No newline at end of file + [TestFixture] + [Category("MariaDB")] + public class Default : Generic.GenericDatabase + { + protected override IGrateTestContext Context => GrateTestContext.MariaDB; + } + + [TestFixture] + [Category("MariaDBCaseInsensitive")] + public class CaseInsensitive : Generic.GenericDatabase + { + protected override IGrateTestContext Context => GrateTestContext.MariaDBCaseInsensitive; + } +} diff --git a/grate.unittests/MariaDB/SetupTestEnvironment.cs b/grate.unittests/MariaDB/SetupTestEnvironment.cs index 72c418ce..ea227205 100644 --- a/grate.unittests/MariaDB/SetupTestEnvironment.cs +++ b/grate.unittests/MariaDB/SetupTestEnvironment.cs @@ -3,10 +3,21 @@ namespace grate.unittests.MariaDB; -[SetUpFixture] -[Category("MariaDB")] -public class SetupTestEnvironment : Generic.SetupDockerTestEnvironment +public class SetupTestEnvironment { - protected override IGrateTestContext GrateTestContext => unittests.GrateTestContext.MariaDB; - protected override IDockerTestContext DockerTestContext => unittests.GrateTestContext.MariaDB; -} \ No newline at end of file + [SetUpFixture] + [Category("MariaDB")] + public class Default : Generic.SetupDockerTestEnvironment + { + protected override IGrateTestContext GrateTestContext => unittests.GrateTestContext.MariaDB; + protected override IDockerTestContext DockerTestContext => unittests.GrateTestContext.MariaDB; + } + + [SetUpFixture] + [Category("MariaDBCaseInsensitive")] + public class CaseInsensitive : Generic.SetupDockerTestEnvironment + { + protected override IGrateTestContext GrateTestContext => unittests.GrateTestContext.MariaDBCaseInsensitive; + protected override IDockerTestContext DockerTestContext => unittests.GrateTestContext.MariaDBCaseInsensitive; + } +} diff --git a/grate.unittests/SqLite/Database.cs b/grate.unittests/SqLite/Database.cs index c3afa494..22d5f910 100644 --- a/grate.unittests/SqLite/Database.cs +++ b/grate.unittests/SqLite/Database.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Data.Common; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -16,12 +17,16 @@ public class Database: Generic.GenericDatabase protected override async Task CreateDatabase(string db) { - await using var conn = new SqliteConnection(Context.ConnectionString(db)); - conn.Open(); - await using var cmd = conn.CreateCommand(); - var sql = "CREATE TABLE dummy(name VARCHAR(1))"; - cmd.CommandText = sql; - await cmd.ExecuteNonQueryAsync(); + try + { + await using var conn = new SqliteConnection(Context.ConnectionString(db)); + conn.Open(); + await using var cmd = conn.CreateCommand(); + var sql = "CREATE TABLE dummy(name VARCHAR(1))"; + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + } + catch (DbException) { } } protected override async Task> GetDatabases() @@ -36,4 +41,4 @@ protected override async Task> GetDatabases() } protected override bool ThrowOnMissingDatabase => false; -} \ No newline at end of file +} diff --git a/grate.unittests/TestContext.cs b/grate.unittests/TestContext.cs index 43f03bf9..695a88ca 100644 --- a/grate.unittests/TestContext.cs +++ b/grate.unittests/TestContext.cs @@ -8,6 +8,8 @@ public static class GrateTestContext internal static readonly OracleGrateTestContext Oracle = new(); internal static readonly PostgreSqlGrateTestContext PostgreSql = new(); // ReSharper disable once InconsistentNaming - internal static readonly MariaDbGrateTestContext MariaDB = new(); + internal static readonly MariaDbGrateTestContext MariaDB = new(true); + // ReSharper disable once InconsistentNaming + internal static readonly MariaDbGrateTestContext MariaDBCaseInsensitive = new(false); internal static readonly SqliteGrateTestContext Sqlite = new(); } diff --git a/grate.unittests/TestInfrastructure/IGrateTestContext.cs b/grate.unittests/TestInfrastructure/IGrateTestContext.cs index 9ff55e2c..e5114a0f 100644 --- a/grate.unittests/TestInfrastructure/IGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/IGrateTestContext.cs @@ -84,4 +84,6 @@ public GrateMigrator GetMigrator(string databaseName, KnownFolders knownFolders, } bool SupportsCreateDatabase { get; } + + bool DatabaseNamingCaseSensitive { get; } } diff --git a/grate.unittests/TestInfrastructure/MariaDbGrateTestContext.cs b/grate.unittests/TestInfrastructure/MariaDbGrateTestContext.cs index f5178789..e7b2cfa1 100644 --- a/grate.unittests/TestInfrastructure/MariaDbGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/MariaDbGrateTestContext.cs @@ -10,11 +10,18 @@ namespace grate.unittests.TestInfrastructure; class MariaDbGrateTestContext : TestContextBase, IGrateTestContext, IDockerTestContext { + public MariaDbGrateTestContext(bool databaseNamingCaseSensitive) + { + DatabaseNamingCaseSensitive = databaseNamingCaseSensitive; + } + public string AdminPassword { get; set; } = default!; public int? Port { get; set; } public string DockerCommand(string serverName, string adminPassword) => - $"run -d --name {serverName} -e MYSQL_ROOT_PASSWORD={adminPassword} -P mariadb:10.5.9"; + DatabaseNamingCaseSensitive + ? $"run -d --name {serverName} -e MYSQL_ROOT_PASSWORD={adminPassword} -P mariadb:10.5.9" + : $"run -d --name {serverName} -e MYSQL_ROOT_PASSWORD={adminPassword} -P mariadb:10.5.9 --lower_case_table_names=1"; public string AdminConnectionString => $"Server=localhost;Port={Port};Database=mysql;Uid=root;Pwd={AdminPassword}"; public string ConnectionString(string database) => $"Server=localhost;Port={Port};Database={database};Uid=root;Pwd={AdminPassword}"; @@ -37,7 +44,7 @@ public string DockerCommand(string serverName, string adminPassword) => SleepTwoSeconds = "SELECT SLEEP(2);" }; - public string ExpectedVersionPrefix => "10.5.9-MariaDB"; public bool SupportsCreateDatabase => true; + public bool DatabaseNamingCaseSensitive { get; } } diff --git a/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs b/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs index 8934c502..4c3c2498 100644 --- a/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs @@ -39,4 +39,5 @@ public string DockerCommand(string serverName, string adminPassword) => public string ExpectedVersionPrefix => "Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production"; public bool SupportsCreateDatabase => true; + public bool DatabaseNamingCaseSensitive => false; } diff --git a/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs b/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs index 78317598..2a8892b8 100644 --- a/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs @@ -40,4 +40,5 @@ public string DockerCommand(string serverName, string adminPassword) => public string ExpectedVersionPrefix => "PostgreSQL 14."; public bool SupportsCreateDatabase => true; + public bool DatabaseNamingCaseSensitive => true; } diff --git a/grate.unittests/TestInfrastructure/SqLiteGrateTestContext.cs b/grate.unittests/TestInfrastructure/SqLiteGrateTestContext.cs index 491f8d21..b61e3d4d 100644 --- a/grate.unittests/TestInfrastructure/SqLiteGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/SqLiteGrateTestContext.cs @@ -36,4 +36,5 @@ class SqliteGrateTestContext : TestContextBase, IGrateTestContext public string ExpectedVersionPrefix => "3.32.3"; public bool SupportsCreateDatabase => false; -} \ No newline at end of file + public bool DatabaseNamingCaseSensitive => false; +} diff --git a/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs b/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs index 14c4a0fe..cadb9da0 100644 --- a/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs @@ -39,4 +39,5 @@ public string DockerCommand(string serverName, string adminPassword) => public string ExpectedVersionPrefix => "Microsoft SQL Server 2019"; public bool SupportsCreateDatabase => true; + public bool DatabaseNamingCaseSensitive => false; } diff --git a/grate/Migration/MariaDbDatabase.cs b/grate/Migration/MariaDbDatabase.cs index eb54d121..9eaacd62 100644 --- a/grate/Migration/MariaDbDatabase.cs +++ b/grate/Migration/MariaDbDatabase.cs @@ -1,5 +1,8 @@ -using System.Data.Common; +using System.Data; +using System.Data.Common; +using System.Linq; using System.Threading.Tasks; +using Dapper; using grate.Infrastructure; using Microsoft.Extensions.Logging; using MySqlConnector; @@ -20,4 +23,24 @@ public override Task RestoreDatabase(string backupPath) { throw new System.NotImplementedException("Restoring a database from file is not currently supported for Maria DB."); } -} \ No newline at end of file + + public override async Task DatabaseExists() + { + var sql = $"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '{DatabaseName}'"; + try + { + await OpenConnection(); + var results = await Connection.QueryAsync(sql, commandType: CommandType.Text); + return results.Any(); + } + catch (DbException ex) + { + Logger.LogDebug(ex, "An unexpected error occurred performing the CheckDatabaseExists check: {ErrorMessage}", ex.Message); + return false; // base method also returns false on any DbException + } + finally + { + await CloseConnection(); + } + } +}