Skip to content

Commit

Permalink
Solve erikbra#245 without new command-line parameter (erikbra#256)
Browse files Browse the repository at this point in the history
* Solve erikbra#245 without new command-line parameter
* Ignore the standard tables casing issue for Oracle, as it has never been case sensitive
* Fixed casing of INFORMATION_SCHEMA in unit tests
  • Loading branch information
erikbra authored Jun 3, 2023
1 parent a61b6e0 commit 1870912
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 29 deletions.
93 changes: 93 additions & 0 deletions grate.unittests/Generic/GenericMigrationTables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,90 @@ public async Task Migration_does_not_fail_if_table_already_exists(string tableNa
Assert.DoesNotThrowAsync(() => migrator.Migrate());
}
}

[TestCase("version")]
[TestCase("vErSiON")]
public async Task Does_not_create_Version_table_if_it_exists_with_another_casing(string existingTable)
{
await CheckTableCasing("Version", existingTable, (config, name) => config.VersionTableName = name);
}

[TestCase("scriptsrun")]
[TestCase("SCRiptSrUN")]
public async Task Does_not_create_ScriptsRun_table_if_it_exists_with_another_casing(string existingTable)
{
await CheckTableCasing("ScriptsRun", existingTable, (config, name) => config.ScriptsRunTableName = name);
}

[TestCase("scriptsrunerrors")]
[TestCase("ScripTSRunErrors")]
public async Task Does_not_create_ScriptsRunErrors_table_if_it_exists_with_another_casing(string existingTable)
{
await CheckTableCasing("ScriptsRunErrors", existingTable, (config, name) => config.ScriptsRunErrorsTableName = name);
}

protected virtual async Task CheckTableCasing(string tableName, string funnyCasing, Action<GrateConfiguration, string> setTableName)
{
var db = TestConfig.RandomDatabase();

var parent = TestConfig.CreateRandomTempDirectory();
var knownFolders = FoldersConfiguration.Default();

// Set the version table name to be lower-case first, and run one migration.
var config = Context.GetConfiguration(db, parent, knownFolders);

setTableName(config, funnyCasing);

await using (var migrator = Context.GetMigrator(config))
{
await migrator.Migrate();
}

// Check that the table is indeed created with lower-case
var errorCaseCountAfterFirstMigration = await TableCountIn(db, funnyCasing);
var normalCountAfterFirstMigration = await TableCountIn(db, tableName);
Assert.Multiple(() =>
{
errorCaseCountAfterFirstMigration.Should().Be(1);
normalCountAfterFirstMigration.Should().Be(0);
});

// Run migration again - make sure it does not create the table with different casing too
setTableName(config, tableName);
await using (var migrator = Context.GetMigrator(config))
{
await migrator.Migrate();
}

var errorCaseCountAfterSecondMigration = await TableCountIn(db, funnyCasing);
var normalCountAfterSecondMigration = await TableCountIn(db, tableName);
Assert.Multiple(() =>
{
errorCaseCountAfterSecondMigration.Should().Be(1);
normalCountAfterSecondMigration.Should().Be(0);
});

}

private async Task<int> TableCountIn(string db, string tableName)
{
var schemaName = Context.DefaultConfiguration.SchemaName;
var supportsSchemas = Context.DatabaseMigrator.SupportsSchemas;

var fullTableName = supportsSchemas ? tableName : Context.Syntax.TableWithSchema(schemaName, tableName);
var tableSchema = supportsSchemas ? schemaName : db;

int count;
string countSql = CountTableSql(tableSchema, fullTableName);

await using (var conn = Context.GetDbConnection(Context.ConnectionString(db)))
{
count = await conn.ExecuteScalarAsync<int>(countSql);
}

return count;
}

[Test()]
public async Task Inserts_version_in_version_table()
{
Expand Down Expand Up @@ -156,4 +239,14 @@ protected static DirectoryInfo MakeSurePathExists(DirectoryInfo? path)
private static DirectoryInfo Wrap(DirectoryInfo root, string? relativePath) =>
new(Path.Combine(root.ToString(), relativePath ?? ""));

protected virtual string CountTableSql(string schemaName, string tableName)
{
return $@"
SELECT count(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE
table_schema = '{schemaName}' AND
table_name = '{tableName}'
";
}

}
21 changes: 19 additions & 2 deletions grate.unittests/Oracle/MigrationTables.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
using System;
using System.Threading.Tasks;
using grate.Configuration;
using grate.unittests.TestInfrastructure;
using NUnit.Framework;

namespace grate.unittests.Oracle;

[TestFixture]
[Category("Oracle")]
public class MigrationTables: Generic.GenericMigrationTables
public class MigrationTables : Generic.GenericMigrationTables
{
protected override IGrateTestContext Context => GrateTestContext.Oracle;
}

protected override Task CheckTableCasing(string tableName, string funnyCasing, Action<GrateConfiguration, string> setTableName)
{
Assert.Ignore("Oracle has never been case-sensitive for grate. No need to introduce that now.");
return Task.CompletedTask;
}

protected override string CountTableSql(string schemaName, string tableName)
{
return $@"
SELECT COUNT(table_name) FROM user_tables
WHERE
lower(table_name) = '{tableName.ToLowerInvariant()}'";
}
}
11 changes: 10 additions & 1 deletion grate.unittests/SqLite/MigrationTables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,13 @@ namespace grate.unittests.Sqlite;
public class MigrationTables: Generic.GenericMigrationTables
{
protected override IGrateTestContext Context => GrateTestContext.Sqlite;
}

protected override string CountTableSql(string schemaName, string tableName)
{
return $@"
SELECT COUNT(name) FROM sqlite_master
WHERE type ='table' AND
name = '{tableName}';
";
}
}
12 changes: 11 additions & 1 deletion grate.unittests/SqlServer/MigrationTables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,14 @@ namespace grate.unittests.SqlServer;
public class MigrationTables: Generic.GenericMigrationTables
{
protected override IGrateTestContext Context => GrateTestContext.SqlServer;
}

protected override string CountTableSql(string schemaName, string tableName)
{
return $@"
SELECT count(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE
TABLE_SCHEMA = '{schemaName}' AND
TABLE_NAME = '{tableName}' COLLATE Latin1_General_CS_AS
";
}
}
4 changes: 4 additions & 0 deletions grate.unittests/TestContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using grate.unittests.TestInfrastructure;
using NUnit.Framework;

// There are some parallelism issues, but this does not solve it
//[assembly:LevelOfParallelism(1)]

namespace grate.unittests;

Expand Down
11 changes: 11 additions & 0 deletions grate.unittests/TestInfrastructure/IGrateTestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ DefaultConfiguration with
SqlFilesDirectory = sqlFilesDirectory
};

public GrateConfiguration GetConfiguration(string databaseName, DirectoryInfo sqlFilesDirectory,
IFoldersConfiguration knownFolders, string? env, bool runInTransaction) =>
DefaultConfiguration with
{
ConnectionString = ConnectionString(databaseName),
Folders = knownFolders,
Environment = env != null ? new GrateEnvironment(env) : null,
Transaction = runInTransaction,
SqlFilesDirectory = sqlFilesDirectory
};

public GrateMigrator GetMigrator(GrateConfiguration config)
{
var factory = Substitute.For<IFactory>();
Expand Down
4 changes: 4 additions & 0 deletions grate/Configuration/GrateConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public record GrateConfiguration
public string? ConnectionString { get; init; } = null;

public string SchemaName { get; init; } = "grate";

public string ScriptsRunTableName { get; set; } = "ScriptsRun";
public string ScriptsRunErrorsTableName { get; set; } = "ScriptsRunErrors";
public string VersionTableName { get; set; } = "Version";

public string? AdminConnectionString
{
Expand Down
65 changes: 48 additions & 17 deletions grate/Migration/AnsiSqlDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,42 @@ protected AnsiSqlDatabase(ILogger logger, ISyntax syntax)
.Split("=", TrimEntries | RemoveEmptyEntries).Last();

public abstract bool SupportsDdlTransactions { get; }
protected abstract bool SupportsSchemas { get; }
public abstract bool SupportsSchemas { get; }
public bool SplitBatchStatements => true;

public string StatementSeparatorRegex => _syntax.StatementSeparatorRegex;

public string ScriptsRunTable => _syntax.TableWithSchema(SchemaName, "ScriptsRun");
public string ScriptsRunErrorsTable => _syntax.TableWithSchema(SchemaName, "ScriptsRunErrors");
public string VersionTable => _syntax.TableWithSchema(SchemaName, "Version");
public string ScriptsRunTable => _syntax.TableWithSchema(SchemaName, ScriptsRunTableName);
public string ScriptsRunErrorsTable => _syntax.TableWithSchema(SchemaName, ScriptsRunErrorsTableName);
public string VersionTable => _syntax.TableWithSchema(SchemaName, VersionTableName);

private string ScriptsRunTableName { get; set; }
private string ScriptsRunErrorsTableName { get; set; }
private string VersionTableName { get; set; }

public virtual Task InitializeConnections(GrateConfiguration configuration)
{
Logger.LogInformation("Initializing connections.");

ConnectionString = configuration.ConnectionString;
AdminConnectionString = configuration.AdminConnectionString;

SchemaName = configuration.SchemaName;

VersionTableName = configuration.VersionTableName;
ScriptsRunTableName = configuration.ScriptsRunTableName;
ScriptsRunErrorsTableName = configuration.ScriptsRunErrorsTableName;

Config = configuration;

return Task.CompletedTask;
}

private async Task<string> ExistingOrDefault(string schemaName, string tableName) =>
await ExistingTable(schemaName, tableName) ?? tableName;



private string? AdminConnectionString { get; set; }
protected string? ConnectionString { get; set; }

Expand Down Expand Up @@ -266,6 +282,10 @@ private async Task<bool> RunSchemaExists()

protected virtual async Task CreateScriptsRunTable()
{
// Update scripts run table name with the correct casing, should it differ from the standard

ScriptsRunTableName = await ExistingOrDefault(SchemaName, ScriptsRunTableName);

string createSql = $@"
CREATE TABLE {ScriptsRunTable}(
{_syntax.PrimaryKeyColumn("id")},
Expand All @@ -288,6 +308,9 @@ protected virtual async Task CreateScriptsRunTable()

protected virtual async Task CreateScriptsRunErrorsTable()
{
// Update scripts run errors table name with the correct casing, should it differ from the standard
ScriptsRunErrorsTableName = await ExistingOrDefault(SchemaName, ScriptsRunErrorsTableName);

string createSql = $@"
CREATE TABLE {ScriptsRunErrorsTable}(
{_syntax.PrimaryKeyColumn("id")},
Expand All @@ -310,6 +333,9 @@ protected virtual async Task CreateScriptsRunErrorsTable()

protected virtual async Task CreateVersionTable()
{
// Update version table name with the correct casing, should it differ from the standard
VersionTableName = await ExistingOrDefault(SchemaName, VersionTableName);

string createSql = $@"
CREATE TABLE {VersionTable}(
{_syntax.PrimaryKeyColumn("id")},
Expand All @@ -320,6 +346,7 @@ protected virtual async Task CreateVersionTable()
entered_by {_syntax.VarcharType}(50) NULL
{_syntax.PrimaryKeyConstraint("Version", "id")}
)";

if (!await VersionTableExists())
{
await ExecuteNonQuery(ActiveConnection, createSql, Config?.CommandTimeout);
Expand All @@ -338,21 +365,25 @@ ALTER TABLE {VersionTable}
}
}

protected async Task<bool> ScriptsRunTableExists() => await TableExists(SchemaName, "ScriptsRun");
protected async Task<bool> ScriptsRunErrorsTableExists() => await TableExists(SchemaName, "ScriptsRunErrors");
public async Task<bool> VersionTableExists() => await TableExists(SchemaName, "Version");
protected async Task<bool> StatusColumnInVersionTableExists() => await ColumnExists(SchemaName, "Version", "status");
protected async Task<bool> ScriptsRunTableExists() => (await ExistingTable(SchemaName, ScriptsRunTableName) is not null) ;
protected async Task<bool> ScriptsRunErrorsTableExists() => (await ExistingTable(SchemaName, ScriptsRunErrorsTableName) is not null);
public async Task<bool> VersionTableExists() => (await ExistingTable(SchemaName, VersionTableName) is not null);

protected async Task<bool> StatusColumnInVersionTableExists() => await ColumnExists(SchemaName, VersionTableName, "status");

public async Task<bool> TableExists(string schemaName, string tableName)
public async Task<string?> ExistingTable(string schemaName, string tableName)
{
var fullTableName = SupportsSchemas ? tableName : _syntax.TableWithSchema(schemaName, tableName);
var tableSchema = SupportsSchemas ? schemaName : DatabaseName;

string existsSql = ExistsSql(tableSchema, fullTableName);

var res = await ExecuteScalarAsync<object>(ActiveConnection, existsSql);

return !DBNull.Value.Equals(res) && res is not null;
var name = (!DBNull.Value.Equals(res) && res is not null) ? (string) res : null;

var prefix = SupportsSchemas ? string.Empty : _syntax.TableWithSchema(schemaName, string.Empty);
return name?[prefix.Length..] ;
}

private async Task<bool> ColumnExists(string schemaName, string tableName, string columnName)
Expand All @@ -369,10 +400,10 @@ private async Task<bool> ColumnExists(string schemaName, string tableName, strin
protected virtual string ExistsSql(string tableSchema, string fullTableName)
{
return $@"
SELECT * FROM INFORMATION_SCHEMA.TABLES
SELECT table_name FROM INFORMATION_SCHEMA.TABLES
WHERE
TABLE_SCHEMA = '{tableSchema}' AND
TABLE_NAME = '{fullTableName}'
LOWER(TABLE_SCHEMA) = LOWER('{tableSchema}') AND
LOWER(TABLE_NAME) = LOWER('{fullTableName}')
";
}

Expand All @@ -381,9 +412,9 @@ protected virtual string ExistsSql(string tableSchema, string fullTableName, str
return $@"
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = '{tableSchema}' AND
TABLE_NAME = '{fullTableName}' AND
COLUMN_NAME = '{columnName}'
LOWER(TABLE_SCHEMA) = LOWER('{tableSchema}') AND
LOWER(TABLE_NAME) = LOWER('{fullTableName}') AND
LOWER(COLUMN_NAME) = LOWER('{columnName}')
";
}

Expand Down
2 changes: 2 additions & 0 deletions grate/Migration/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public interface IDatabase : IAsyncDisposable
public string ScriptsRunErrorsTable { get; }
public string VersionTable { get; }
DbConnection ActiveConnection { set; }
bool SupportsSchemas { get; }

Task InitializeConnections(GrateConfiguration configuration);
Task OpenConnection();
Expand Down Expand Up @@ -48,4 +49,5 @@ Task InsertScriptRun(string scriptName, string? sql, string hash, bool runOnce,
void SetDefaultConnectionActive();
Task<IDisposable> OpenNewActiveConnection();
Task OpenActiveConnection();
Task<string?> ExistingTable(string schemaName, string tableName);
}
2 changes: 1 addition & 1 deletion grate/Migration/MariaDbDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public MariaDbDatabase(ILogger<MariaDbDatabase> logger)
{ }

public override bool SupportsDdlTransactions => false;
protected override bool SupportsSchemas => false;
public override bool SupportsSchemas => false;
protected override DbConnection GetSqlConnection(string? connectionString) => new MySqlConnection(connectionString);

public override Task RestoreDatabase(string backupPath)
Expand Down
4 changes: 2 additions & 2 deletions grate/Migration/OracleDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public OracleDatabase(ILogger<OracleDatabase> logger)
}

public override bool SupportsDdlTransactions => false;
protected override bool SupportsSchemas => false;
public override bool SupportsSchemas => false;

protected override DbConnection GetSqlConnection(string? connectionString) => new OracleConnection(connectionString);

protected override string ExistsSql(string tableSchema, string fullTableName) =>
$@"
SELECT * FROM user_tables
SELECT table_name FROM user_tables
WHERE
lower(table_name) = '{fullTableName.ToLowerInvariant()}'
";
Expand Down
Loading

0 comments on commit 1870912

Please sign in to comment.