Skip to content

Commit

Permalink
Post-migration actions and post-migration database views refreshing (#…
Browse files Browse the repository at this point in the history
…112)

* Added post-migration actions and post-migration database views refresh

* Added refreshing views script to resources of Kros.KORM, other small changes

* Added option to accept and use the script ID up to which actions will be executed after migration. Changed the script for refreshing views.

* small fixes, added unit test

* added info to readme.md, changed the version

* added info to readme.md, changed the version

* changed version

* Small fixes to README and unit test for Migration Runner

* update of README

---------

Co-authored-by: Kantorová Michaela <[email protected]>
  • Loading branch information
mchlkntrv and Kantorová Michaela authored Jan 14, 2025
1 parent d02b228 commit fd6d222
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 4 deletions.
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.
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)
{
await database.ExecuteNonQueryAsync("INSERT ...");
}
});
})
.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)

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
50 changes: 48 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,31 @@ 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)
{
await db.ExecuteNonQueryAsync("CREATE TABLE Departments (Id int);");
}

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

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;

0 comments on commit fd6d222

Please sign in to comment.