Skip to content

Commit

Permalink
Support *.post.database.create.sql migration scripts (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
chullybun committed Dec 21, 2022
1 parent e46261b commit 7ae2c85
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 22 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Represents the **NuGet** versions.

## 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).

## v2.2.0
- *Enhancement:* Enable `Parameters` to be passed via the command line; either adding, or overridding any pre-configured values. Use `-p|--param Name=Value` syntax; e.g. `--param JournalSchema=dbo`.
- *Enhancement:* Enable moustache syntax property placeholder replacements (e.g`{{ParameterName}}`), from the `Parameters`, within SQL scripts to allow changes during execution into the database at runtime.
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.2.0</Version>
<Version>2.3.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ A migration script can contain basic moustache value replacement syntax such as

It is recommended that each script be enclosed by a transaction that can be rolled back in the case of error; otherwise, a script could be partially applied and will then need manual intervention to resolve.

_Note_: There are _special case_ scripts that will be executed pre- and post- migrations. In that any scripts ending with `.pre.deploy.sql` will always be executed before the migrations are attempted, and any scripts ending with `.post.deploy.sql` will always be executed after all the migrations have successfully executed.
_Note_: There are _special case_ scripts that will be executed pre- and post- migration deployments. In that any scripts ending with `.pre.deploy.sql` will always be executed before the migrations are attempted, and any scripts ending with `.post.deploy.sql` will always be executed after all the migrations have successfully executed. Finally, any scripts ending with `.post.database.create.sql` will only be executed when (after) the database is created.

<br/>

Expand Down
65 changes: 45 additions & 20 deletions src/DbEx/Migration/DatabaseMigrationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace DbEx.Migration
public abstract class DatabaseMigrationBase
{
private const string NothingFoundText = " ** Nothing found. **";
private const string OnDatabaseCreateName = "post.database.create";
private HandlebarsCodeGenerator? _dataCodeGen;
private bool _hasInitialized = false;

Expand Down Expand Up @@ -328,7 +329,7 @@ protected virtual async Task<bool> ExecuteScriptsAsync(IEnumerable<DatabaseMigra
}

if (includeExecutionLogging)
Logger.LogInformation(" {Content} {Tag}", script.Name, script.Tag ?? "");
Logger.LogInformation("{Content}", $" {script.Name}{(string.IsNullOrEmpty(script.Tag) ? "" : $" > {script.Tag}")}");

try
{
Expand All @@ -345,7 +346,7 @@ protected virtual async Task<bool> ExecuteScriptsAsync(IEnumerable<DatabaseMigra
}

if (includeExecutionLogging && !somethingExecuted)
Logger.LogInformation(" {Content}", "No new scripts found to execute.");
Logger.LogInformation("{Content}", " No new scripts found to execute.");

return true;
}
Expand Down Expand Up @@ -380,19 +381,19 @@ protected virtual async Task<bool> DatabaseExistsAsync(CancellationToken cancell
/// <para>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).</para></remarks>
protected virtual async Task<bool> DatabaseDropAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation(" Drop database...");
Logger.LogInformation("{Content}", " Drop database...");

var exists = await DatabaseExistsAsync(cancellationToken).ConfigureAwait(false);
if (!exists)
{
Logger.LogInformation(" {Content}", $"Database '{DatabaseName}' does not exist and therefore not dropped.");
Logger.LogInformation("{Content}", $" Database '{DatabaseName}' does not exist and therefore not dropped.");
return true;
}

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

Logger.LogInformation(" {Content}", $"Database '{DatabaseName}' dropped.");
Logger.LogInformation("{Content}", $" Database '{DatabaseName}' dropped.");
return true;
}

Expand All @@ -405,20 +406,39 @@ protected virtual async Task<bool> DatabaseDropAsync(CancellationToken cancellat
/// <para>The <c>@DatabaseName</c> literal within the resulting (embedded resource) 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).</para></remarks>
protected virtual async Task<bool> DatabaseCreateAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation(" Create database...");
Logger.LogInformation("{Content}", " Create database...");

var exists = await DatabaseExistsAsync(cancellationToken).ConfigureAwait(false);
if (exists)
{
Logger.LogInformation(" {Content}", $"Database '{DatabaseName}' already exists and therefore not created.");
Logger.LogInformation("{Content}", $" Database '{DatabaseName}' already exists and therefore not created.");
return true;
}

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

Logger.LogInformation(" {Content}", $"Database '{DatabaseName}' did not exist and was created.");
return true;
Logger.LogInformation("{Content}", $" Database '{DatabaseName}' did not exist and was created.");
Logger.LogInformation("{Content}", string.Empty);
Logger.LogInformation("{Content}", $" Probing for '{OnDatabaseCreateName}' embedded resources: {string.Join(", ", GetNamespacesWithSuffix($"{MigrationsNamespace}.*.sql"))}");

var scripts = new List<DatabaseMigrationScript>();
foreach (var ass in Args.Assemblies)
{
foreach (var name in ass.GetManifestResourceNames().Where(rn => Namespaces.Any(ns => rn.StartsWith($"{ns}.{MigrationsNamespace}.", StringComparison.InvariantCulture) && rn.EndsWith($".{OnDatabaseCreateName}.sql", StringComparison.InvariantCultureIgnoreCase))).OrderBy(x => x))
{
scripts.Add(new DatabaseMigrationScript(ass, name) { RunAlways = true });
}
}

if (scripts.Count == 0)
{
Logger.LogInformation("{Content}", NothingFoundText);
return true;
}

Logger.LogInformation("{Content}", " Execute the embedded resources...");
return await ExecuteScriptsAsync(scripts, true, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -433,11 +453,14 @@ private async Task<bool> DatabaseMigrateAsync(CancellationToken cancellationToke
{
foreach (var name in ass.GetManifestResourceNames().Where(rn => Namespaces.Any(ns => rn.StartsWith($"{ns}.{MigrationsNamespace}.", StringComparison.InvariantCulture))).OrderBy(x => x))
{
// Ignore any/all database create scripts.
if (name.EndsWith($".{OnDatabaseCreateName}.sql", StringComparison.InvariantCultureIgnoreCase))
continue;

// Determine run order and add script to list.
var order = name.EndsWith(".pre.deploy.sql", StringComparison.InvariantCultureIgnoreCase) ? 1 :
name.EndsWith(".post.deploy.sql", StringComparison.InvariantCultureIgnoreCase) ? 3 : 2;

using var sr = new StreamReader(ass.GetManifestResourceStream(name)!);
scripts.Add(new DatabaseMigrationScript(ass, name) { GroupOrder = order, RunAlways = order != 2 });
}
}
Expand All @@ -448,7 +471,7 @@ private async Task<bool> DatabaseMigrateAsync(CancellationToken cancellationToke
return true;
}

Logger.LogInformation("{Content}", " Migrate the embedded resources...");
Logger.LogInformation("{Content}", " Execute the embedded resources...");
return await ExecuteScriptsAsync(scripts, true, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -512,7 +535,7 @@ private async Task<bool> DatabaseSchemaAsync(CancellationToken cancellationToken
// Make sure there is work to be done.
if (scripts.Count == 0)
{
Logger.LogInformation(NothingFoundText);
Logger.LogInformation("{Content}", NothingFoundText);
return true;
}

Expand Down Expand Up @@ -546,7 +569,8 @@ protected virtual async Task<bool> DatabaseSchemaAsync(List<DatabaseMigrationScr
// Drop all existing (in reverse order).
int i = 0;
var ss = new List<DatabaseMigrationScript>();
Logger.LogInformation(" Drop known schema objects...");
Logger.LogInformation("{Content}", string.Empty);
Logger.LogInformation("{Content}", " Drop known schema objects...");
foreach (var sor in list.OrderByDescending(x => x.SchemaOrder).ThenByDescending(x => x.TypeOrder).ThenByDescending(x => x.Schema).ThenByDescending(x => x.Name))
{
ss.Add(new DatabaseMigrationScript(sor.SqlDropStatement, sor.SqlDropStatement) { GroupOrder = i++, RunAlways = true });
Expand All @@ -558,7 +582,8 @@ protected virtual async Task<bool> DatabaseSchemaAsync(List<DatabaseMigrationScr
// Execute each migration script proper (i.e. create 'em as scripted).
i = 0;
ss.Clear();
Logger.LogInformation(" Create known schema objects...");
Logger.LogInformation("{Content}", string.Empty);
Logger.LogInformation("{Content}", " Create known schema objects...");
foreach (var sor in list.OrderBy(x => x.SchemaOrder).ThenBy(x => x.TypeOrder).ThenBy(x => x.Schema).ThenBy(x => x.Name))
{
var migrationScript = sor.MigrationScript;
Expand Down Expand Up @@ -612,18 +637,18 @@ private DatabaseSchemaScriptBase ValidateAndReadySchemaScript(DatabaseSchemaScri
/// <remarks>This is invoked by using the <see cref="CommandExecuteAsync(string, Func{CancellationToken, Task{bool}}, Func{string}?, CancellationToken)"/>.</remarks>
protected virtual async Task<bool> DatabaseResetAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation(" Querying database to infer table(s) schema...");
Logger.LogInformation("{Content}", " Querying database to infer table(s) schema...");

var tables = await Database.SelectSchemaAsync(DatabaseSchemaConfig, Args.DataParserArgs, cancellationToken).ConfigureAwait(false);
var query = tables.Where(DataResetFilterPredicate);
if (Args.DataResetFilterPredicate != null)
query = query.Where(Args.DataResetFilterPredicate);

Logger.LogInformation(" Deleting data from all tables (except filtered)...");
Logger.LogInformation("{Content}", " Deleting data from all tables (except filtered)...");
var delete = query.Where(x => !x.IsAView).ToList();
if (delete.Count == 0)
{
Logger.LogInformation(" None.");
Logger.LogInformation("{Content}", " None.");
return true;
}

Expand Down Expand Up @@ -677,7 +702,7 @@ private async Task<bool> DatabaseDataAsync(CancellationToken cancellationToken)
}

// Infer database schema.
Logger.LogInformation(" Querying database to infer table(s)/column(s) schema...");
Logger.LogInformation("{Content}", " Querying database to infer table(s)/column(s) schema...");
var dbTables = await Database.SelectSchemaAsync(DatabaseSchemaConfig, Args.DataParserArgs, cancellationToken).ConfigureAwait(false);

// Iterate through each resource - parse the data, then insert/merge as requested.
Expand Down Expand Up @@ -739,7 +764,7 @@ protected virtual async Task<bool> DatabaseDataAsync(List<DataTable> dataTables,

foreach (var table in dataTables)
{
Logger.LogInformation("");
Logger.LogInformation("{Content}", string.Empty);
Logger.LogInformation("{Content}", $"---- Executing {table.Schema}{(table.Schema == string.Empty ? "" : ".")}{table.Name} SQL:");

var sql = _dataCodeGen.Generate(table);
Expand Down Expand Up @@ -864,7 +889,7 @@ private async Task<bool> ExecuteSqlStatementsInternalAsync(string[]? statements,
{
if (statements == null || statements.Length == 0)
{
Logger.LogInformation(" No statements to execute.");
Logger.LogInformation("{Content}", " No statements to execute.");
return true;
}

Expand Down
1 change: 1 addition & 0 deletions tests/DbEx.Test.Console/DbEx.Test.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<None Remove="Data\Other.sql" />
<None Remove="Migrations\000.post.database.create.sql" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT 1

0 comments on commit 7ae2c85

Please sign in to comment.