Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post-migration actions and post-migration database views refreshing #112

Merged
merged 12 commits into from
Jan 14, 2025
Merged
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,33 @@ using (var transaction = database.BeginTransaction(IsolationLevel.Chaos))
}
```

### Post-migration actions
Option for post-migration actions and post-migration database views refresh with an option to accept the script ID up to which actions will be executed after migration is available. The default script RefreshViews.sql for refreshing views is in resources.
mchlkntrv marked this conversation as resolved.
Show resolved Hide resolved
Action will be executed only if the ID of the latest script of the migration is less than or equal to the user-defined ID. If the ID is not defined, the action will execute regardless.

```CSharp
const long scriptId = 20251912010;

builder.Services.AddKorm(builder.Configuration)
.AddKormMigrations(options =>
{
var assembly = Assembly.GetEntryAssembly();
options.AddAssemblyScriptsProvider(assembly, "CompanyStruct.SqlScripts");

options.AddRefreshViewsAction();

options.AddAfterMigrationAction(async (database, id) =>
{
if (id <= scriptId)
{
database.ExecuteNonQuery("INSERT ...");
mchlkntrv marked this conversation as resolved.
Show resolved Hide resolved
}
await Task.CompletedTask;
});
})
.Migrate();
```

### Record types

KORM supports a new `record` type for model definition.
Expand Down
4 changes: 3 additions & 1 deletion src/Kros.KORM.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<Version>7.0.1</Version>
<Version>7.1.0</Version>
<Authors>KROS a. s.</Authors>
<Company>KROS a. s.</Company>
<Description>KORM is fast, easy to use, micro ORM tool (Kros Object Relation Mapper).</Description>
Expand Down Expand Up @@ -63,9 +63,11 @@

<ItemGroup>
<None Remove="Resources\MigrationsHistoryTableScript.sql" />
<None Remove="Resources\RefreshViews.sql" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\MigrationsHistoryTableScript.sql" />
<EmbeddedResource Include="Resources\RefreshViews.sql" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
Expand Down
39 changes: 38 additions & 1 deletion src/Migrations/MigrationOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Kros.KORM.Migrations.Providers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Kros.KORM.Migrations
{
Expand All @@ -11,14 +14,23 @@ namespace Kros.KORM.Migrations
public class MigrationOptions
{
private const int DefaultTimeoutInSeconds = 30;
private const string DefaultResourceNamespace = "Resources";
private const string DefaultRefreshViewsScriptName = "RefreshViews.sql";

private List<IMigrationScriptsProvider> _providers = new List<IMigrationScriptsProvider>();
private List<IMigrationScriptsProvider> _providers = [];
private List<Func<IDatabase, long, Task>> _actions = [];

/// <summary>
/// List of <see cref="IMigrationScriptsProvider"/>.
/// </summary>
public IEnumerable<IMigrationScriptsProvider> Providers => _providers;


/// <summary>
/// List of actions to be executed on database after migration scripts are executed.
/// </summary>
public IEnumerable<Func<IDatabase, long, Task>> Actions => _actions;

/// <summary>
/// Timeout for the migration script command.
/// If not set, default value 30s will be used.
Expand Down Expand Up @@ -46,5 +58,30 @@ public void AddAssemblyScriptsProvider(Assembly assembly, string resourceNamespa
/// <param name="folderPath">Path to folder where migration scripts are stored.</param>
public void AddFileScriptsProvider(string folderPath)
=> AddScriptsProvider(new FileMigrationScriptsProvider(folderPath));

/// <summary>
/// Add action to be executed on database after migration scripts are executed.
/// </summary>
/// <param name="actionToExecute"></param>
public void AddAfterMigrationAction(Func<IDatabase, long, Task> actionToExecute)
{
_actions.Add(actionToExecute);
}

/// <summary>
/// Add action of refreshing all database views.
/// </summary>
public void AddRefreshViewsAction()
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"{assembly.GetName().Name}.{DefaultResourceNamespace}.{DefaultRefreshViewsScriptName}";
AddAfterMigrationAction(async (database, _) =>
{
await using Stream resourceStream = assembly.GetManifestResourceStream(resourceName);
using var reader = new StreamReader(resourceStream, Encoding.UTF8);
string script = await reader.ReadToEndAsync();
await database.ExecuteNonQueryAsync(script);
});
}
}
}
6 changes: 6 additions & 0 deletions src/Migrations/MigrationsRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public async Task MigrateAsync()
if (migrationScripts.Any())
{
await ExecuteMigrationScripts(helper.Database, migrationScripts);
long maxScriptId = migrationScripts.Max(s => s.Id);

foreach (var action in _migrationOptions.Actions)
{
await action(helper.Database, maxScriptId);
}
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/Resources/RefreshViews.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
DECLARE @sql NVARCHAR(MAX)
mchlkntrv marked this conversation as resolved.
Show resolved Hide resolved

SET @sql = (
SELECT STRING_AGG('EXEC sp_refreshview ' + QUOTENAME(name), '; ')
FROM sys.views
)

EXEC sp_executesql @sql
1 change: 1 addition & 0 deletions tests/Kros.KORM.UnitTests/Kros.KORM.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<EmbeddedResource Include="Resources\ScriptsForRunner\MigrateToLastVersion\20190301003_AddContactsTable.sql">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\ScriptsForRunner\MigrateWithActions\20250108001_AddPeopleColumnAge.sql" />
<EmbeddedResource Include="SqlScripts\20190228001_InitDatabase.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
Expand Down
51 changes: 49 additions & 2 deletions tests/Kros.KORM.UnitTests/Migrations/MigrationsRunnerShould.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ [Id] ASC
INSERT INTO __KormMigrationsHistory VALUES (20190228002, 'Old', 'FromUnitTests', '20190228')
INSERT INTO __KormMigrationsHistory VALUES (20190301001, 'InitDatabase', 'FromUnitTests', '20190301')";

private readonly static string CreateView_People =
$@"CREATE VIEW PeopleView AS
SELECT *
FROM dbo.People";
#endregion

protected override string BaseConnectionString => IntegrationTestConfig.ConnectionString;
Expand Down Expand Up @@ -74,14 +78,28 @@ public async Task MigrateToLastVersion()
DatabaseVersionShouldBe(20190301003);
}

[Fact]
public async Task MigrateWithActions()
{
var runner = CreateMigrationsRunner(nameof(MigrateWithActions), true);
InitDatabase();

await runner.MigrateAsync();

ColumnInViewShouldExist("PeopleView", "Age");
TableShouldExist("Roles");
}

private void InitDatabase()
{
ExecuteCommand((cmd) =>
{
foreach (var script in new[] {
CreateTable_MigrationHistory ,
CreateTable_People,
InsertIntoMigrationHistory })
InsertIntoMigrationHistory,
CreateView_People
})
{
cmd.CommandText = script;
cmd.ExecuteScalar();
Expand All @@ -99,6 +117,16 @@ private void TableShouldExist(string tableName)
});
}

private void ColumnInViewShouldExist(string viewName, string columnName)
{
ExecuteCommand((cmd) =>
{
cmd.CommandText = $"SELECT Count(*) FROM sys.columns WHERE object_id = OBJECT_ID('{viewName}') AND name = '{columnName}'";
((int)cmd.ExecuteScalar())
.Should().Be(1);
});
}

private void ExecuteCommand(Action<SqlCommand> action)
{
using (ConnectionHelper.OpenConnection(ServerHelper.Connection))
Expand All @@ -118,13 +146,32 @@ private void DatabaseVersionShouldBe(long databaseVersion)
});
}

private MigrationsRunner CreateMigrationsRunner(string folderName)
private MigrationsRunner CreateMigrationsRunner(string folderName, bool migrateWithActions = false)
{
var options = new MigrationOptions();
options.AddAssemblyScriptsProvider(
Assembly.GetExecutingAssembly(),
$"Kros.KORM.UnitTests.Resources.ScriptsForRunner.{folderName}");

if (migrateWithActions)
{
options.AddRefreshViewsAction();

options.AddAfterMigrationAction(async (db, id) =>
{
if (id <= 20250101001)
{
db.ExecuteNonQuery("CREATE TABLE Departments (Id int);");
mchlkntrv marked this conversation as resolved.
Show resolved Hide resolved
}

if (id <= 20990101001)
{
db.ExecuteNonQuery("CREATE TABLE Roles (Id int);");
}
await Task.CompletedTask;
});
}

return new MigrationsRunner(ServerHelper.Connection.ConnectionString, options);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE People ADD Age INT;