Skip to content

Commit

Permalink
Bug #557: Fixed new (similar) bug if grate schemas/tables exist with …
Browse files Browse the repository at this point in the history
…different casing for PostgreSQL and MariaDB> (#560)

There was a regression, a similar bug to #245 which was introduced when using SQL scripts to handle the grate table structure versioning (grate the grate), in release 1.7.0.  This should fix it.

Fixes #557
  • Loading branch information
erikbra authored Jul 22, 2024
1 parent 133da40 commit c5a1740
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 11 deletions.
10 changes: 8 additions & 2 deletions src/grate.core/Migration/GrateMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,10 @@ private async Task<GrateConfiguration> GetBootstrapInternalGrateConfiguration(st
[
"ScriptsRunTable=GrateScriptsRun",
"ScriptsRunErrorsTable=GrateScriptsRunErrors",
"VersionTable=GrateVersion"
"VersionTable=GrateVersion",
"ScriptsRunTableLowerCase=grateversion",
"ScriptsRunErrorsTableLowerCase=gratescriptrunerrors",
"VersionTableLowerCase=grateversion"
],
DeferWritingToRunTables = true,
Environment = GrateEnvironment.InternalBootstrap,
Expand Down Expand Up @@ -664,7 +667,10 @@ private async Task<GrateConfiguration> GetInternalGrateConfiguration(string inte
UserTokens = [
$"ScriptsRunTable={thisConfig.ScriptsRunTableName}",
$"ScriptsRunErrorsTable={thisConfig.ScriptsRunErrorsTableName}",
$"VersionTable={thisConfig.VersionTableName}"
$"VersionTable={thisConfig.VersionTableName}",
$"ScriptsRunTableLowerCase={thisConfig.ScriptsRunTableName.ToLower()}",
$"ScriptsRunErrorsTableLowerCase={thisConfig.ScriptsRunErrorsTableName.ToLower()}",
$"VersionTableLowerCase={thisConfig.VersionTableName.ToLower()}"
],

Environment = GrateEnvironment.Internal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS `{{SchemaName}}_{{VersionTableLowerCase}}`
RENAME TO `{{SchemaName}}_{{VersionTable}}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS `{{SchemaName}}_{{ScriptsRunTableLowerCase}}`
RENAME TO `{{SchemaName}}_{{ScriptsRunTable}}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS `{{SchemaName}}_{{ScriptsRunErrorsTableLowerCase}}`
RENAME TO `{{SchemaName}}_{{ScriptsRunErrorsTable}}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DO LANGUAGE plpgsql
$$
BEGIN
ALTER SCHEMA {{SchemaName}} RENAME TO "{{SchemaName}}";
EXCEPTION WHEN duplicate_schema or invalid_schema_name THEN
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DO LANGUAGE plpgsql
$$
BEGIN
ALTER TABLE "{{SchemaName}}"."{{VersionTableLowerCase}}" RENAME TO "{{VersionTable}}";
EXCEPTION WHEN undefined_table or duplicate_table THEN
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DO LANGUAGE plpgsql
$$
BEGIN
ALTER TABLE "{{SchemaName}}"."{{ScriptsRunTableLowerCase}}" RENAME TO "{{ScriptsRunTable}}";
EXCEPTION WHEN undefined_table or duplicate_table THEN
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DO LANGUAGE plpgsql
$$
BEGIN
ALTER TABLE "{{SchemaName}}"."{{ScriptsRunErrorsTableLowerCase}}" RENAME TO "{{ScriptsRunErrorsTable}}";
EXCEPTION WHEN undefined_table or duplicate_table THEN
END;
$$;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Sqlite.TestInfrastructure;
using grate.Configuration;
using Sqlite.TestInfrastructure;
using TestCommon.TestInfrastructure;

namespace Sqlite.Bootstrapping;
Expand All @@ -9,7 +10,6 @@ namespace Sqlite.Bootstrapping;
public class When_Grate_structure_is_not_latest_version(SqliteGrateTestContext context, ITestOutputHelper testOutput)
: TestCommon.Generic.Bootstrapping.When_Grate_structure_is_not_latest_version(context, testOutput)
{
[Fact(Skip = "Not able to apply logic to DDL statements for Sqlite")]
public override Task The_latest_version_is_applied() => Task.CompletedTask;
public override Task The_latest_version_is_applied(string versionTableName) => Task.CompletedTask;
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@
namespace TestCommon.Generic.Bootstrapping;

// ReSharper disable once UnusedType.Global
// ReSharper disable once InconsistentNaming
public abstract class When_Grate_structure_is_not_latest_version(IGrateTestContext context, ITestOutputHelper testOutput)
: MigrationsScriptsBase(context, testOutput)
{

[Fact]
public virtual async Task The_latest_version_is_applied()
[Theory]
[MemberData(nameof(VersionTableWithDifferentCasings))]
public virtual async Task The_latest_version_is_applied(string versionTableName)
{
var db = TestConfig.RandomDatabase();
var parent = CreateRandomTempDirectory();

// This will create the version table without the status column
// This will create the version table without the status column, with lower-case
var cfg = Context.DefaultConfiguration with
{
VersionTableName = versionTableName.ToLower()
};

var config = GrateConfigurationBuilder.Create(Context.DefaultConfiguration)
var config = GrateConfigurationBuilder.Create(cfg)
.WithConnectionString(Context.ConnectionString(db))
.WithFolders(FoldersConfiguration.Default())
.WithSqlFilesDirectory(parent)
Expand Down Expand Up @@ -69,7 +75,6 @@ public virtual async Task The_latest_version_is_applied()

conn.Close();


// Check that the status column is not there
var tableWithSchema = Context.Syntax.TableWithSchema(config.SchemaName, config.VersionTableName);
var selectSql = $"SELECT * FROM {tableWithSchema}";
Expand All @@ -82,13 +87,24 @@ public virtual async Task The_latest_version_is_applied()
TryClose(conn);
columns.Should().NotContain("status".ToUpper());

// Reset the config to use the actual column casings, to make sure that the status column is added even if the
// already "existing" tables (which we just created above, with varying casings), are updated, even if the
// casing of the default configuration is different from the standard one
var actualConfig = config with
{
VersionTableName = versionTableName
};

// Run the migration
await using (var migrator = Context.Migrator.WithConfiguration(config))
await using (var migrator = Context.Migrator.WithConfiguration(actualConfig))
{
await RunMigration(migrator);
}

// Check that the status column has been added
tableWithSchema = Context.Syntax.TableWithSchema(actualConfig.SchemaName, actualConfig.VersionTableName);
selectSql = $"SELECT * FROM {tableWithSchema}";

conn = Context.CreateDbConnection(db);
reader = await conn.ExecuteReaderAsync(selectSql);

Expand All @@ -99,8 +115,140 @@ public virtual async Task The_latest_version_is_applied()

//await Context.DropDatabase(db);
}


[Theory]
[MemberData(nameof(TablesWithDifferentCasings))]
public virtual async Task The_table_name_casings_are_converted_if_needed(
string scriptsRun, string scriptsRunErrors, string version)
{
var db = TestConfig.RandomDatabase();
var parent = CreateRandomTempDirectory();

// This will create the ScriptsRun, ScriptsRunErrors and Version tables
// with lower-case version of the supplied casing
var cfg = Context.DefaultConfiguration with
{
VersionTableName = version.ToLower(),
ScriptsRunTableName = scriptsRun.ToLower(),
ScriptsRunErrorsTableName = scriptsRunErrors.ToLower(),
};

var config = GrateConfigurationBuilder.Create(cfg)
.WithConnectionString(Context.ConnectionString(db))
.WithFolders(FoldersConfiguration.Default())
.WithSqlFilesDirectory(parent)
.Build();

// Create database
var password = Context.AdminConnectionString
.Split(";", TrimEntries | RemoveEmptyEntries)
.SingleOrDefault(entry => entry.StartsWith("Password") || entry.StartsWith("Pwd"))?
.Split("=", TrimEntries | RemoveEmptyEntries)
.Last();

var createDatabaseSql = Context.Syntax.CreateDatabase(db, password);
using (var adminConn = Context.CreateAdminDbConnection())
{
await adminConn.ExecuteAsync(createDatabaseSql);
}

var conn = Context.CreateDbConnection(db);

// Manually create the script tables, with modified casing
var resources = TestInfrastructure.Bootstrapping.GetBootstrapScripts(this.Context.DatabaseType, "Baseline");
foreach (var resource in resources)
{
var resourceText = await TestInfrastructure.Bootstrapping.GetContent(this.Context.DatabaseType.Assembly, resource);

resourceText = resourceText.Replace("{{ScriptsRunTable}}", config.ScriptsRunTableName);
resourceText = resourceText.Replace("{{ScriptsRunErrorsTable}}", config.ScriptsRunErrorsTableName);
resourceText = resourceText.Replace("{{VersionTable}}", config.VersionTableName);
resourceText = resourceText.Replace("{{SchemaName}}", config.SchemaName);

await conn.ExecuteAsync(resourceText);
}

conn.Close();

// Check that the table exists in the DB, with lower-case
// - Selecting from the table with a non-existing table name would throw an exception
await CheckThatTableExists(config.VersionTableName, config.SchemaName);
await CheckThatTableExists(config.ScriptsRunTableName, config.SchemaName);
await CheckThatTableExists(config.ScriptsRunErrorsTableName, config.SchemaName);

// Reset the config to use the supplied column names, and run the migration. Then, select from the version table
// with the supplied table name, and see that it has been converted to the default casing (if needed by the DB provider),
// i.e., we can still select from it.
var actualConfig = config with
{
VersionTableName = version,
ScriptsRunTableName = scriptsRun,
ScriptsRunErrorsTableName = scriptsRunErrors,
};

// Run the migration
await using (var migrator = Context.Migrator.WithConfiguration(actualConfig))
{
await RunMigration(migrator);
}

// Check that the tables can be selected from, using the supplied table names.
await CheckThatTableExists(actualConfig.VersionTableName, config.SchemaName);
await CheckThatTableExists(actualConfig.ScriptsRunTableName, config.SchemaName);
await CheckThatTableExists(actualConfig.ScriptsRunErrorsTableName, config.SchemaName);

return;

async Task CheckThatTableExists(string tableName, string schemaName)
{
var tableWithSchema = Context.Syntax.TableWithSchema(schemaName, tableName);
var selectSql = $"SELECT * FROM {tableWithSchema}";

conn = Context.CreateDbConnection(db);
var reader = await conn.ExecuteReaderAsync(selectSql);

var columns = GetColumns(reader);
TryClose(conn);
columns.Should().HaveCountGreaterThan(2);
}
}

public static TheoryData<string> VersionTableWithDifferentCasings()
{
var def = GrateConfiguration.Default;
return new TheoryData<string>
{
def.VersionTableName,
def.VersionTableName.ToLower(),
def.VersionTableName.ToUpper()
};
}

public static TheoryData<string, string, string> TablesWithDifferentCasings()
{
var def = GrateConfiguration.Default;

var all = from scriptsRun in DifferentPermutations(def.ScriptsRunTableName)
from scriptsRunErrors in DifferentPermutations(def.ScriptsRunErrorsTableName)
from versionTableName in DifferentPermutations(def.VersionTableName)
select (scriptsRun, scriptsRunErrors, versionTableName);

var data = new TheoryData<string, string, string>();
foreach (var (run, errors, version) in all)
{
data.Add(run, errors, version);
}
return data;
}

private static IEnumerable<string> DifferentPermutations(string original) =>
[
original,
original.ToLower(),
//original.ToUpper(),
new string(original.ToCharArray().Select(c => Random.Shared.Next(2) == 0 ? c : Char.ToUpper(c)).ToArray())
];

private async Task RunMigration(IGrateMigrator migrator)
{
Expand Down

0 comments on commit c5a1740

Please sign in to comment.