Skip to content

Commit

Permalink
Fix minor issues. (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
chullybun committed Jan 3, 2023
1 parent 7ae2c85 commit 1daec86
Show file tree
Hide file tree
Showing 13 changed files with 89 additions and 51 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Represents the **NuGet** versions.

## v2.3.1
- *Fixed:* Added `IDisposable` to `DatabaseMigrationBase` to ensure underlying database connections are correctly disposed (via `IDatabase.Dispose`).
- *Fixed:* `DatabaseJournal` updated to use `DatabaseMigrationBase.ReplaceSqlRuntimeParameters` versus own limited implementation.
- *Fixed:* Throw `InvalidOperationException` versus _errant_ `NullReferenceException` where required embedded resource is not found.

## v2.3.0
- *Enhancement:* Support the execution of `*.post.database.create.sql` migration scripts that will _only_ get invoked after the creation of the database (i.e. a potential one-time only execution).

Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>2.3.0</Version>
<Version>2.3.1</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
4 changes: 2 additions & 2 deletions src/DbEx.MySql/MySqlSchemaConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
using CoreEx.Database;
using DbEx.DbSchema;
using DbEx.Migration.Data;
using OnRamp.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using DbEx.Migration;

namespace DbEx.MySql
{
Expand Down Expand Up @@ -107,7 +107,7 @@ public override DbColumnSchema CreateColumnFromInformationSchema(DbTableSchema t
public override async Task LoadAdditionalInformationSchema(IDatabase database, List<DbTableSchema> tables, DataParserArgs? dataParserArgs, CancellationToken cancellationToken)
{
// Configure all the single column foreign keys.
using var sr3 = StreamLocator.GetResourcesStreamReader("SelectTableForeignKeys.sql", new Assembly[] { typeof(MySqlSchemaConfig).Assembly }).StreamReader!;
using var sr3 = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTableForeignKeys.sql", new Assembly[] { typeof(MySqlSchemaConfig).Assembly });
var fks = await database.SqlStatement(await sr3.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr => new
{
ConstraintName = dr.GetValue<string>("fk_constraint_name"),
Expand Down
2 changes: 1 addition & 1 deletion src/DbEx.SqlServer/Migration/SqlServerMigration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected override async Task<bool> DatabaseResetAsync(CancellationToken cancell
{
// Filter out temporal tables.
Logger.LogInformation(" Querying database to find and filter all temporal table(s)...");
using var sr = StreamLocator.GetResourcesStreamReader($"DatabaseTemporal.sql", ArtefactResourceAssemblies.ToArray()).StreamReader!;
using var sr = GetRequiredResourcesStreamReader($"DatabaseTemporal.sql", ArtefactResourceAssemblies.ToArray());
await Database.SqlStatement(sr.ReadToEnd()).SelectQueryAsync(dr =>
{
_resetBypass.Add($"[{dr.GetValue<string>("schema")}].[{dr.GetValue<string>("table")}]");
Expand Down
9 changes: 5 additions & 4 deletions src/DbEx.SqlServer/SqlServerSchemaConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using CoreEx.Database;
using DbEx.DbSchema;
using DbEx.Migration;
using DbEx.Migration.Data;
using OnRamp.Utility;
using System;
Expand Down Expand Up @@ -83,7 +84,7 @@ public override DbColumnSchema CreateColumnFromInformationSchema(DbTableSchema t
public override async Task LoadAdditionalInformationSchema(IDatabase database, List<DbTableSchema> tables, DataParserArgs? dataParserArgs, CancellationToken cancellationToken)
{
// Configure all the single column foreign keys.
using var sr3 = StreamLocator.GetResourcesStreamReader("SelectTableForeignKeys.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly }).StreamReader!;
using var sr3 = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTableForeignKeys.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly });
var fks = await database.SqlStatement(await sr3.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr => new
{
ConstraintName = dr.GetValue<string>("FK_CONSTRAINT_NAME"),
Expand Down Expand Up @@ -113,7 +114,7 @@ public override async Task LoadAdditionalInformationSchema(IDatabase database, L
}

// Select the table identity columns.
using var sr4 = StreamLocator.GetResourcesStreamReader("SelectTableIdentityColumns.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly }).StreamReader!;
using var sr4 = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTableIdentityColumns.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly });
await database.SqlStatement(await sr4.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr =>
{
var t = tables.SingleOrDefault(x => x.Schema == dr.GetValue<string>("TABLE_SCHEMA") && x.Name == dr.GetValue<string>("TABLE_NAME"));
Expand All @@ -128,7 +129,7 @@ public override async Task LoadAdditionalInformationSchema(IDatabase database, L
}, cancellationToken).ConfigureAwait(false);

// Select the "always" generated columns.
using var sr5 = StreamLocator.GetResourcesStreamReader("SelectTableAlwaysGeneratedColumns.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly }).StreamReader!;
using var sr5 = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTableAlwaysGeneratedColumns.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly });
await database.SqlStatement(await sr5.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr =>
{
var t = tables.SingleOrDefault(x => x.Schema == dr.GetValue<string>("TABLE_SCHEMA") && x.Name == dr.GetValue<string>("TABLE_NAME"));
Expand All @@ -141,7 +142,7 @@ public override async Task LoadAdditionalInformationSchema(IDatabase database, L
}, cancellationToken).ConfigureAwait(false);

// Select the generated columns.
using var sr6 = StreamLocator.GetResourcesStreamReader("SelectTableGeneratedColumns.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly }).StreamReader!;
using var sr6 = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTableGeneratedColumns.sql", new Assembly[] { typeof(SqlServerSchemaConfig).Assembly });
await database.SqlStatement(await sr6.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr =>
{
var t = tables.SingleOrDefault(x => x.Schema == dr.GetValue<string>("TABLE_SCHEMA") && x.Name == dr.GetValue<string>("TABLE_NAME"));
Expand Down
4 changes: 2 additions & 2 deletions src/DbEx/Console/MigrationConsoleBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ protected ValidationResult ValidateMultipleValue(string option, ValidationContex
try
{
// Create the migrator.
var migrator = CreateMigrator();
using var migrator = CreateMigrator();

// Write header, etc.
if (!BypassOnWrites)
Expand Down Expand Up @@ -370,7 +370,7 @@ protected virtual async Task<bool> OnMigrateAsync(DatabaseMigrationBase migrator
/// <summary>
/// Creates the <see cref="DatabaseMigrationBase"/> that is used to perform the database migration orchestration.
/// </summary>
/// <returns></returns>
/// <returns>The <see cref="DatabaseMigrationBase"/>.</returns>
protected abstract DatabaseMigrationBase CreateMigrator();

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/DbEx/DatabaseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

using CoreEx.Database;
using DbEx.DbSchema;
using DbEx.Migration;
using DbEx.Migration.Data;
using OnRamp.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -37,7 +37,7 @@ public static async Task<List<DbTableSchema>> SelectSchemaAsync(this IDatabase d
var refDataPredicate = new Func<DbTableSchema, bool>(t => t.Columns.Any(c => c.Name == refDataCodeColumn && !c.IsPrimaryKey && c.DotNetType == "string") && t.Columns.Any(c => c.Name == refDataTextColumn && !c.IsPrimaryKey && c.DotNetType == "string"));

// Get all the tables and their columns.
using var sr = StreamLocator.GetResourcesStreamReader("SelectTableAndColumns.sql", new Assembly[] { typeof(DatabaseExtensions).Assembly }).StreamReader!;
using var sr = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTableAndColumns.sql", new Assembly[] { typeof(DatabaseExtensions).Assembly });
await database.SqlStatement(await sr.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr =>
{
if (!databaseSchemaConfig.SupportsSchema && dr.GetValue<string>("TABLE_SCHEMA") != databaseSchemaConfig.DatabaseName)
Expand Down Expand Up @@ -70,7 +70,7 @@ public static async Task<List<DbTableSchema>> SelectSchemaAsync(this IDatabase d
}

// Configure all the single column primary and unique constraints.
using var sr2 = StreamLocator.GetResourcesStreamReader("SelectTablePrimaryKey.sql", new Assembly[] { typeof(DatabaseExtensions).Assembly }).StreamReader!;
using var sr2 = DatabaseMigrationBase.GetRequiredResourcesStreamReader("SelectTablePrimaryKey.sql", new Assembly[] { typeof(DatabaseExtensions).Assembly });
var pks = await database.SqlStatement(await sr2.ReadToEndAsync().ConfigureAwait(false)).SelectQueryAsync(dr => new
{
ConstraintName = dr.GetValue<string>("CONSTRAINT_NAME"),
Expand Down
22 changes: 8 additions & 14 deletions src/DbEx/Migration/DatabaseJournal.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/DbEx

using CoreEx.Database;
using OnRamp.Utility;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -44,16 +43,16 @@ public async Task EnsureExistsAsync(CancellationToken cancellationToken = defaul
if (_journalExists)
return;

using var sr = StreamLocator.GetResourcesStreamReader($"JournalExists.sql", Migrator.ArtefactResourceAssemblies.ToArray()).StreamReader!;
var exists = await Migrator.Database.SqlStatement(ReplacePlacholders(sr.ReadToEnd())).ScalarAsync<object?>(cancellationToken).ConfigureAwait(false);
using var sr = DatabaseMigrationBase.GetRequiredResourcesStreamReader($"JournalExists.sql", Migrator.ArtefactResourceAssemblies.ToArray())!;
var exists = await Migrator.Database.SqlStatement(Migrator.ReplaceSqlRuntimeParameters(sr.ReadToEnd())).ScalarAsync<object?>(cancellationToken).ConfigureAwait(false);
if (exists != null)
{
_journalExists = true;
return;
}

using var sr2 = StreamLocator.GetResourcesStreamReader($"JournalCreate.sql", Migrator.ArtefactResourceAssemblies.ToArray()).StreamReader!;
await Migrator.Database.SqlStatement(ReplacePlacholders(sr2.ReadToEnd())).NonQueryAsync(cancellationToken).ConfigureAwait(false);
using var sr2 = DatabaseMigrationBase.GetRequiredResourcesStreamReader($"JournalCreate.sql", Migrator.ArtefactResourceAssemblies.ToArray())!;
await Migrator.Database.SqlStatement(Migrator.ReplaceSqlRuntimeParameters(sr2.ReadToEnd())).NonQueryAsync(cancellationToken).ConfigureAwait(false);

Migrator.Logger.LogInformation(" *Journal table did not exist within the database and was automatically created.");

Expand All @@ -65,8 +64,8 @@ public async Task AuditScriptExecutionAsync(DatabaseMigrationScript script, Canc
{
await EnsureExistsAsync(cancellationToken).ConfigureAwait(false);

using var sr = StreamLocator.GetResourcesStreamReader($"JournalAudit.sql", Migrator.ArtefactResourceAssemblies.ToArray()).StreamReader!;
await Migrator.Database.SqlStatement(ReplacePlacholders(sr.ReadToEnd()))
using var sr = DatabaseMigrationBase.GetRequiredResourcesStreamReader($"JournalAudit.sql", Migrator.ArtefactResourceAssemblies.ToArray())!;
await Migrator.Database.SqlStatement(Migrator.ReplaceSqlRuntimeParameters(sr.ReadToEnd()))
.Param("@scriptname", script.Name)
.Param("@applied", DateTime.UtcNow)
.NonQueryAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -77,13 +76,8 @@ public async Task<IEnumerable<string>> GetExecutedScriptsAsync(CancellationToken
{
await EnsureExistsAsync(cancellationToken).ConfigureAwait(false);

using var sr = StreamLocator.GetResourcesStreamReader($"JournalPrevious.sql", Migrator.ArtefactResourceAssemblies.ToArray()).StreamReader!;
return await Migrator.Database.SqlStatement(ReplacePlacholders(sr.ReadToEnd())).SelectQueryAsync(dr => dr.GetValue<string>("scriptname"), cancellationToken).ConfigureAwait(false);
using var sr = DatabaseMigrationBase.GetRequiredResourcesStreamReader($"JournalPrevious.sql", Migrator.ArtefactResourceAssemblies.ToArray())!;
return await Migrator.Database.SqlStatement(Migrator.ReplaceSqlRuntimeParameters(sr.ReadToEnd())).SelectQueryAsync(dr => dr.GetValue<string>("scriptname"), cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Replace the placeholders.
/// </summary>
private string ReplacePlacholders(string sql) => string.IsNullOrEmpty(sql) ? sql : sql.Replace("{{DatabaseName}}", Migrator.DatabaseName).Replace("{{JournalSchema}}", Schema).Replace("{{JournalTable}}", Table);
}
}
50 changes: 43 additions & 7 deletions src/DbEx/Migration/DatabaseMigrationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,29 @@ namespace DbEx.Migration
/// <summary>
/// Represents the base capabilities for the database migration orchestrator leveraging <see href="https://dbup.readthedocs.io/en/latest/">DbUp</see> (where applicable).
/// </summary>
public abstract class DatabaseMigrationBase
public abstract class DatabaseMigrationBase : IDisposable
{
private const string NothingFoundText = " ** Nothing found. **";
private const string OnDatabaseCreateName = "post.database.create";
private HandlebarsCodeGenerator? _dataCodeGen;
private bool _hasInitialized = false;

/// <summary>
/// Gets the <b>Resource</b> content from the file system and then <c>Resources</c> folder within the <paramref name="assemblies"/> until found.
/// </summary>
/// <param name="fileName">The file name.</param>
/// <param name="assemblies">Assemblies to use to probe for assembly resource (in defined sequence).</param>
/// <param name="extensions">The file extensions to also probe for.</param>
/// <returns>The resource <see cref="StreamReader"/> where found; otherwise, throws <see cref="ArgumentException"/>.</returns>
public static StreamReader GetRequiredResourcesStreamReader(string fileName, Assembly[]? assemblies = null, string[]? extensions = null)
{
var result = StreamLocator.GetResourcesStreamReader(fileName, assemblies, extensions);
if (result.StreamReader == null)
throw new InvalidOperationException($"Embedded resource '{fileName}' is required and was not found within the selected assemblies.");

return result.StreamReader;
}

/// <summary>
/// Initializes an instance of the <see cref="DatabaseMigrationBase"/> class.
/// </summary>
Expand Down Expand Up @@ -367,7 +383,7 @@ protected virtual async Task<bool> ExecuteScriptsAsync(IEnumerable<DatabaseMigra
/// <remarks>The <c>@DatabaseName</c> literal within the resulting (embedded resource) command is replaced by the <see cref="DatabaseName"/> using a <see cref="string.Replace(string, string)"/> (i.e. not database parameterized as not all databases support).</remarks>
protected virtual async Task<bool> DatabaseExistsAsync(CancellationToken cancellationToken = default)
{
using var sr = StreamLocator.GetResourcesStreamReader($"DatabaseExists.sql", ArtefactResourceAssemblies.ToArray()).StreamReader!;
using var sr = GetRequiredResourcesStreamReader($"DatabaseExists.sql", ArtefactResourceAssemblies.ToArray());
var name = await MasterDatabase.SqlStatement(ReplaceSqlRuntimeParameters(sr.ReadToEnd())).ScalarAsync<string?>(cancellationToken);
return name != null;
}
Expand All @@ -390,7 +406,7 @@ protected virtual async Task<bool> DatabaseDropAsync(CancellationToken cancellat
return true;
}

using var sr = StreamLocator.GetResourcesStreamReader($"DatabaseDrop.sql", ArtefactResourceAssemblies.ToArray()).StreamReader!;
using var sr = GetRequiredResourcesStreamReader($"DatabaseDrop.sql", ArtefactResourceAssemblies.ToArray());
await MasterDatabase.SqlStatement(ReplaceSqlRuntimeParameters(sr.ReadToEnd())).NonQueryAsync(cancellationToken);

Logger.LogInformation("{Content}", $" Database '{DatabaseName}' dropped.");
Expand All @@ -415,7 +431,7 @@ protected virtual async Task<bool> DatabaseCreateAsync(CancellationToken cancell
return true;
}

using var sr = StreamLocator.GetResourcesStreamReader($"DatabaseCreate.sql", ArtefactResourceAssemblies.ToArray()).StreamReader!;
using var sr = GetRequiredResourcesStreamReader($"DatabaseCreate.sql", ArtefactResourceAssemblies.ToArray());
await MasterDatabase.SqlStatement(ReplaceSqlRuntimeParameters(sr.ReadToEnd())).NonQueryAsync(cancellationToken);

Logger.LogInformation("{Content}", $" Database '{DatabaseName}' did not exist and was created.");
Expand Down Expand Up @@ -652,7 +668,7 @@ protected virtual async Task<bool> DatabaseResetAsync(CancellationToken cancella
return true;
}

using var sr = StreamLocator.GetResourcesStreamReader($"DatabaseReset_sql", ArtefactResourceAssemblies.ToArray(), StreamLocator.HandlebarsExtensions).StreamReader!;
using var sr = GetRequiredResourcesStreamReader($"DatabaseReset_sql", ArtefactResourceAssemblies.ToArray(), StreamLocator.HandlebarsExtensions);
var cg = new HandlebarsCodeGenerator(sr);
var sql = cg.Generate(delete);

Expand Down Expand Up @@ -758,7 +774,7 @@ protected virtual async Task<bool> DatabaseDataAsync(List<DataTable> dataTables,
// Cache the compiled code-gen template.
if (_dataCodeGen == null)
{
using var sr = StreamLocator.GetResourcesStreamReader($"DatabaseData_sql", ArtefactResourceAssemblies.ToArray(), StreamLocator.HandlebarsExtensions).StreamReader!;
using var sr = GetRequiredResourcesStreamReader($"DatabaseData_sql", ArtefactResourceAssemblies.ToArray(), StreamLocator.HandlebarsExtensions);
_dataCodeGen = new HandlebarsCodeGenerator(await sr.ReadToEndAsync().ConfigureAwait(false));
}

Expand Down Expand Up @@ -915,8 +931,28 @@ private async Task<bool> ExecuteSqlStatementsInternalAsync(string[]? statements,
/// </summary>
/// <param name="sql">The SQL command.</param>
/// <returns>The resulting SQL command with runtime replacements make.</returns>
protected string ReplaceSqlRuntimeParameters(string sql) => Args.Parameters.Count == 0
public string ReplaceSqlRuntimeParameters(string sql) => Args.Parameters.Count == 0
? sql : Regex.Replace(sql, "(" + string.Join("|", Args.Parameters.Select(x => $"{{{{{x.Key}}}}}").ToArray()) + ")", m => Args.Parameters.TryGetValue(m.Value[2..^2], out var pv)
? pv?.ToString() : throw new InvalidOperationException($"Runtime Parameter '{m.Value}' found within SQL command; a corresponding Parameter value has not been configured."));

/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Dispose of the resources.
/// </summary>
/// <param name="disposing">Indicates whether to dispose.</param>
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;

Database.Dispose();
MasterDatabase.Dispose();
}
}
}
Loading

0 comments on commit 1daec86

Please sign in to comment.