(bugfix): fluent migrator version info table and columns name s consolidation with the rest of the database tables

This commit is contained in:
Maksym Sadovnychyy 2026-04-26 21:57:43 +02:00
parent bd54f6e6c3
commit 4fc9cfdcea
5 changed files with 52 additions and 45 deletions

View File

@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.3.19] - 2026-04-26
### Changed
- **FluentMigrator / PostgreSQL:** Version metadata table is **`public.version_info`** with snake_case columns **`version`**, **`applied_on`**, **`description`** and unique index **`uc_version`**, configured via **`CertsFluentMigratorVersionTableMetaData`** and **`WithVersionTable`** on the runner (aligned with the rest of the schema naming).
### Removed
- **Startup migrations:** Removed legacy compatibility paths: **`VersionInfo``version_info` rename**, PascalCase → snake_case column repair on the version table, and the **EF-era baseline** that created or seeded **`version_info`** when **`users`** already existed (and **`RunMigrationsService.BaselineVersion`**).
### Upgrade notes
- **Breaking:** **Recreate the Certs engine database** (or use a new empty database). The app no longer upgrades in place from **`VersionInfo`**, mixed column casing, or preFluentMigrator-baseline layouts; **`MigrateUp`** expects a clean or fully FluentMigrator-managed schema.
## [3.3.18] - 2026-04-26
### Changed

View File

@ -31,6 +31,7 @@ public static class ServiceCollectionExtensions {
.ConfigureRunner(rb => rb
.AddPostgres()
.WithGlobalConnectionString(certsEngineConfiguration.ConnectionString)
.WithVersionTable(new CertsFluentMigratorVersionTableMetaData())
.ScanIn(typeof(BaselineCertsSchema).Assembly).For.All())
.AddLogging(lb => lb.AddFluentMigratorConsole());
services.AddScoped<IRunMigrationsService, RunMigrationsService>();

View File

@ -0,0 +1,33 @@
using FluentMigrator.Runner.VersionTableInfo;
namespace MaksIT.CertsUI.Engine.Infrastructure;
/// <summary>FluentMigrator version table: snake_case <c>public.version_info</c> (table and columns) for PostgreSQL consistency.</summary>
public sealed class CertsFluentMigratorVersionTableMetaData : IVersionTableMetaData {
public const string Table = "version_info";
public const string VersionColumn = "version";
public const string AppliedOnColumn = "applied_on";
public const string DescriptionColumn = "description";
public const string UniqueIndex = "uc_version";
public bool OwnsSchema => true;
public string SchemaName => "public";
public string TableName => Table;
public string ColumnName => VersionColumn;
public string DescriptionColumnName => DescriptionColumn;
public string UniqueIndexName => UniqueIndex;
public string AppliedOnColumnName => AppliedOnColumn;
public bool CreateWithPrimaryKey => false;
}

View File

@ -8,7 +8,7 @@ using Npgsql;
namespace MaksIT.CertsUI.Engine.Infrastructure;
/// <summary>
/// FluentMigrator runner for the Certs database: optionally creates the database, baselines legacy EF-created schemas, migrates up,
/// FluentMigrator runner for the Certs database: optionally creates the database, migrates up,
/// then idempotent coordination-table repair. Forward <c>Up()</c> migrations should be additive (new tables/columns); avoid dropping
/// renamed or legacy columns in <c>Up()</c> — use expand/contract and ops-driven cleanup.
/// </summary>
@ -18,8 +18,6 @@ public sealed class RunMigrationsService(
ICertsEngineConfiguration config
) : IRunMigrationsService {
public static long BaselineVersion => BaselineCertsSchema.Version;
public async Task RunAsync(CancellationToken cancellationToken = default) {
if (string.IsNullOrWhiteSpace(config.ConnectionString))
throw new InvalidOperationException(
@ -36,7 +34,6 @@ public sealed class RunMigrationsService(
logger.LogInformation("FluentMigrator discovered {MigrationCount} migration type(s) in {Assembly}.", migrationTypeCount, typeof(BaselineCertsSchema).Assembly.GetName().Name);
await EnsureDatabaseExistsAsync(cancellationToken).ConfigureAwait(false);
await BaselineExistingEfDatabaseAsync(cancellationToken).ConfigureAwait(false);
await Task.Run(() => migrationRunner.MigrateUp(), cancellationToken).ConfigureAwait(false);
await CoordinationTableProvisioner.EnsureAsync(config.ConnectionString, cancellationToken).ConfigureAwait(false);
await VerifyCoreSchemaAsync(cancellationToken).ConfigureAwait(false);
@ -54,7 +51,7 @@ public sealed class RunMigrationsService(
EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'app_runtime_leases')
AND (
EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users')
OR EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'VersionInfo')
OR EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'version_info')
);
""",
conn);
@ -64,7 +61,7 @@ public sealed class RunMigrationsService(
return;
throw new InvalidOperationException(
"After migrations and coordination DDL, schema \"public\" is missing \"app_runtime_leases\" and/or core tables (\"users\" / \"VersionInfo\"). " +
"After migrations and coordination DDL, schema \"public\" is missing \"app_runtime_leases\" and/or core tables (\"users\" / \"version_info\"). " +
"Confirm Database= in the connection string, role CREATE privileges, and that FluentMigrator committed (non-empty connection string).");
}
@ -103,42 +100,4 @@ public sealed class RunMigrationsService(
database);
}
}
/// <summary>
/// If the database already has Certs tables from legacy EF Core migrations, mark the FluentMigrator baseline as applied.
/// </summary>
private async Task BaselineExistingEfDatabaseAsync(CancellationToken cancellationToken) {
await using var conn = new NpgsqlConnection(config.ConnectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using (var cmd = new NpgsqlCommand(
"SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users' LIMIT 1",
conn)) {
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
return;
}
logger.LogInformation("Existing Certs schema detected; baselining FluentMigrator VersionInfo if needed.");
await using (var cmd = new NpgsqlCommand(@"
CREATE TABLE IF NOT EXISTS ""VersionInfo"" (
""Version"" bigint NOT NULL PRIMARY KEY,
""AppliedOn"" timestamp NULL,
""Description"" varchar(1024) NULL
)", conn)) {
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
await using (var cmd = new NpgsqlCommand(
@"INSERT INTO ""VersionInfo"" (""Version"", ""AppliedOn"", ""Description"")
VALUES (@v, @appliedOn, @desc)
ON CONFLICT (""Version"") DO NOTHING",
conn)) {
cmd.Parameters.AddWithValue("v", BaselineCertsSchema.Version);
cmd.Parameters.AddWithValue("appliedOn", DBNull.Value);
cmd.Parameters.AddWithValue("desc", "BaselineCertsSchema (existing DB from EF Core)");
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Version>3.3.18</Version>
<Version>3.3.19</Version>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>