Fix invalid CertificateCheckUrl column errors on existing databases.

Auto-migrate SonicContainer schema on startup, rename legacy typo columns, and use fallback SQL when the column is not yet present.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 13:24:20 +02:00
co-authored by Cursor
parent 76e280d809
commit 0a449727ad
5 changed files with 362 additions and 36 deletions
@@ -12,7 +12,38 @@ GO
IF COL_LENGTH(N'dbo.SonicContainer', N'CertificateCheckUrl') IS NULL
BEGIN
ALTER TABLE [dbo].[SonicContainer]
ADD [CertificateCheckUrl] [nvarchar](500) NULL;
IF COL_LENGTH(N'dbo.SonicContainer', N'vertifiaktechecurl') IS NOT NULL
BEGIN
EXEC sp_rename
N'dbo.SonicContainer.vertifiaktechecurl',
N'CertificateCheckUrl',
N'COLUMN';
END
ELSE IF COL_LENGTH(N'dbo.SonicContainer', N'VerifyCertificateCheckCurl') IS NOT NULL
BEGIN
EXEC sp_rename
N'dbo.SonicContainer.VerifyCertificateCheckCurl',
N'CertificateCheckUrl',
N'COLUMN';
END
ELSE IF COL_LENGTH(N'dbo.SonicContainer', N'CertificateCheckCurl') IS NOT NULL
BEGIN
EXEC sp_rename
N'dbo.SonicContainer.CertificateCheckCurl',
N'CertificateCheckUrl',
N'COLUMN';
END
ELSE IF COL_LENGTH(N'dbo.SonicContainer', N'ZertifikatCheckCurl') IS NOT NULL
BEGIN
EXEC sp_rename
N'dbo.SonicContainer.ZertifikatCheckCurl',
N'CertificateCheckUrl',
N'COLUMN';
END
ELSE
BEGIN
ALTER TABLE [dbo].[SonicContainer]
ADD [CertificateCheckUrl] [nvarchar](500) NULL;
END
END;
GO
@@ -67,6 +67,19 @@ internal static class Program
}
}
try
{
DatabaseSchemaPatcher
.EnsureCertificateCheckUrlColumnAsync(
settings.Database.ConnectionString)
.GetAwaiter()
.GetResult();
}
catch
{
// Ohne ALTER-Rechte weiter starten; Repositories nutzen Fallback-SQL.
}
Application.Run(
new Form1());
}
@@ -0,0 +1,126 @@
using System.Data;
using Microsoft.Data.SqlClient;
namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary>
/// Stellt sicher, dass optionale Schema-Patches auf bestehenden Datenbanken
/// angewendet wurden (z. B. SonicContainer.CertificateCheckUrl).
/// </summary>
public static class DatabaseSchemaPatcher
{
private static readonly string[] LegacyCertificateCheckUrlColumns =
[
"vertifiaktechecurl",
"VerifyCertificateCheckCurl",
"CertificateCheckCurl",
"ZertifikatCheckCurl"
];
private static bool? _hasCertificateCheckUrlColumn;
public static async Task EnsureCertificateCheckUrlColumnAsync(
string connectionString,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(connectionString))
{
return;
}
await using SqlConnection connection = new(connectionString);
await connection.OpenAsync(cancellationToken);
if (await ColumnExistsAsync(
connection,
"CertificateCheckUrl",
cancellationToken))
{
_hasCertificateCheckUrlColumn = true;
return;
}
foreach (string legacyColumn in LegacyCertificateCheckUrlColumns)
{
if (!await ColumnExistsAsync(
connection,
legacyColumn,
cancellationToken))
{
continue;
}
await using SqlCommand renameCommand = new(
"""
EXEC sp_rename
N'dbo.SonicContainer.LEGACY_COLUMN',
N'CertificateCheckUrl',
N'COLUMN';
"""
.Replace(
"LEGACY_COLUMN",
legacyColumn,
StringComparison.Ordinal),
connection);
await renameCommand.ExecuteNonQueryAsync(cancellationToken);
_hasCertificateCheckUrlColumn = true;
return;
}
const string addColumnSql = """
ALTER TABLE [dbo].[SonicContainer]
ADD [CertificateCheckUrl] [nvarchar](500) NULL;
""";
await using SqlCommand addCommand = new(addColumnSql, connection);
await addCommand.ExecuteNonQueryAsync(cancellationToken);
_hasCertificateCheckUrlColumn = true;
}
public static async Task<bool> HasCertificateCheckUrlColumnAsync(
string connectionString,
CancellationToken cancellationToken = default)
{
if (_hasCertificateCheckUrlColumn is bool cached)
{
return cached;
}
if (string.IsNullOrWhiteSpace(connectionString))
{
return false;
}
await using SqlConnection connection = new(connectionString);
await connection.OpenAsync(cancellationToken);
_hasCertificateCheckUrlColumn = await ColumnExistsAsync(
connection,
"CertificateCheckUrl",
cancellationToken);
return _hasCertificateCheckUrlColumn.Value;
}
private static async Task<bool> ColumnExistsAsync(
SqlConnection connection,
string columnName,
CancellationToken cancellationToken)
{
await using SqlCommand command = new(
"SELECT COL_LENGTH(N'dbo.SonicContainer', @ColumnName);",
connection);
command.Parameters.Add(
new SqlParameter("@ColumnName", SqlDbType.NVarChar, 128)
{
Value = columnName
});
object? result =
await command.ExecuteScalarAsync(cancellationToken);
return result is not null and not DBNull;
}
}
@@ -31,7 +31,7 @@ public sealed class DeploymentTargetRepository
nameof(environmentCode));
}
const string sql = """
const string sqlWithCheckUrl = """
SELECT
target.[CertificateTargetId],
target.[TargetDirectory],
@@ -75,11 +75,63 @@ public sealed class DeploymentTargetRepository
target.[TargetFileName];
""";
const string sqlWithoutCheckUrl = """
SELECT
target.[CertificateTargetId],
target.[TargetDirectory],
target.[TargetFileName],
target.[BackupEnabled],
target.[BackupDirectoryName],
target.[IsActive] AS [TargetIsActive],
container.[SonicContainerId],
container.[ContainerName],
container.[ContainerDisplayName],
container.[RestartTimeoutSeconds],
connection.[ConnectionName],
environment.[EnvironmentCode],
environment.[SortOrder] AS [EnvironmentSortOrder],
company.[CompanyCode]
FROM [dbo].[CertificateTarget] AS target
INNER JOIN [dbo].[SonicContainer] AS container
ON container.[SonicContainerId] = target.[SonicContainerId]
INNER JOIN [dbo].[SonicConnection] AS connection
ON connection.[SonicConnectionId] = container.[SonicConnectionId]
INNER JOIN [dbo].[Environment] AS environment
ON environment.[EnvironmentId] = connection.[EnvironmentId]
INNER JOIN [dbo].[Company] AS company
ON company.[CompanyId] = container.[CompanyId]
WHERE target.[IsActive] = 1
AND container.[IsActive] = 1
AND connection.[IsActive] = 1
AND company.[IsActive] = 1
AND environment.[IsActive] = 1
AND environment.[EnvironmentCode] = @EnvironmentCode
ORDER BY
company.[CompanyCode],
environment.[SortOrder],
container.[ContainerName],
target.[TargetDirectory],
target.[TargetFileName];
""";
List<DeploymentTarget> targets = [];
await using SqlConnection connection = new(_connectionString);
await connection.OpenAsync(cancellationToken);
bool hasCertificateCheckUrl =
await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync(
_connectionString,
cancellationToken);
string sql = hasCertificateCheckUrl
? sqlWithCheckUrl
: sqlWithoutCheckUrl;
await using SqlCommand command = new(sql, connection);
command.Parameters.Add(
@@ -120,8 +172,9 @@ public sealed class DeploymentTargetRepository
int containerDisplayNameOrdinal =
reader.GetOrdinal("ContainerDisplayName");
int certificateCheckUrlOrdinal =
reader.GetOrdinal("CertificateCheckUrl");
int? certificateCheckUrlOrdinal = hasCertificateCheckUrl
? reader.GetOrdinal("CertificateCheckUrl")
: null;
int restartTimeoutOrdinal =
reader.GetOrdinal("RestartTimeoutSeconds");
@@ -149,9 +202,11 @@ public sealed class DeploymentTargetRepository
containerDisplayNameOrdinal);
string? certificateCheckUrl =
ReadNullableString(
reader,
certificateCheckUrlOrdinal);
certificateCheckUrlOrdinal is int checkUrlOrdinal
? ReadNullableString(
reader,
checkUrlOrdinal)
: null;
string companyCode =
reader.GetString(companyCodeOrdinal);
@@ -878,7 +878,7 @@ public sealed class SonicSetupRepository
nameof(environmentCode));
}
const string sql = """
const string sqlWithCheckUrl = """
SELECT
container.[SonicContainerId],
container.[SonicConnectionId],
@@ -913,6 +913,40 @@ public sealed class SonicSetupRepository
container.[ContainerName];
""";
const string sqlWithoutCheckUrl = """
SELECT
container.[SonicContainerId],
container.[SonicConnectionId],
container.[CompanyId],
container.[ContainerName],
container.[ContainerDisplayName],
container.[RestartTimeoutSeconds],
company.[CompanyCode],
connection.[ConnectionName]
FROM [dbo].[SonicContainer] AS container
INNER JOIN [dbo].[Company] AS company
ON company.[CompanyId] = container.[CompanyId]
INNER JOIN [dbo].[SonicConnection] AS connection
ON connection.[SonicConnectionId] =
container.[SonicConnectionId]
INNER JOIN [dbo].[Environment] AS environment
ON environment.[EnvironmentId] =
connection.[EnvironmentId]
WHERE container.[IsActive] = 1
AND company.[IsActive] = 1
AND connection.[IsActive] = 1
AND environment.[IsActive] = 1
AND environment.[EnvironmentCode] = @EnvironmentCode
AND
(
@SonicConnectionId IS NULL
OR container.[SonicConnectionId] = @SonicConnectionId
)
ORDER BY
company.[CompanyCode],
container.[ContainerName];
""";
List<ContainerOption> containers = [];
await using SqlConnection connection =
@@ -920,6 +954,15 @@ public sealed class SonicSetupRepository
await connection.OpenAsync(cancellationToken);
bool hasCertificateCheckUrl =
await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync(
_connectionString,
cancellationToken);
string sql = hasCertificateCheckUrl
? sqlWithCheckUrl
: sqlWithoutCheckUrl;
await using SqlCommand command =
new(sql, connection);
@@ -952,8 +995,9 @@ public sealed class SonicSetupRepository
int displayOrdinal =
reader.GetOrdinal("ContainerDisplayName");
int checkUrlOrdinal =
reader.GetOrdinal("CertificateCheckUrl");
int? checkUrlOrdinal = hasCertificateCheckUrl
? reader.GetOrdinal("CertificateCheckUrl")
: null;
containers.Add(
new ContainerOption
@@ -980,9 +1024,10 @@ public sealed class SonicSetupRepository
: reader.GetString(displayOrdinal),
CertificateCheckUrl =
reader.IsDBNull(checkUrlOrdinal)
? null
: reader.GetString(checkUrlOrdinal),
checkUrlOrdinal is int urlOrdinal
&& !reader.IsDBNull(urlOrdinal)
? reader.GetString(urlOrdinal)
: null,
RestartTimeoutSeconds =
reader.GetInt32(
@@ -1328,7 +1373,13 @@ public sealed class SonicSetupRepository
string? checkUrl =
TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl);
const string sql = """
bool hasCertificateCheckUrl =
await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync(
_connectionString,
cancellationToken);
string sql = hasCertificateCheckUrl
? """
INSERT INTO [dbo].[SonicContainer]
(
[CompanyId],
@@ -1354,6 +1405,31 @@ public sealed class SonicSetupRepository
SYSUTCDATETIME(),
SUSER_SNAME()
);
"""
: """
INSERT INTO [dbo].[SonicContainer]
(
[CompanyId],
[SonicConnectionId],
[ContainerName],
[ContainerDisplayName],
[RestartTimeoutSeconds],
[IsActive],
[CreationDateTime],
[CreatedBy]
)
OUTPUT INSERTED.[SonicContainerId]
VALUES
(
@CompanyId,
@SonicConnectionId,
@ContainerName,
@ContainerDisplayName,
@RestartTimeoutSeconds,
1,
SYSUTCDATETIME(),
SUSER_SNAME()
);
""";
await using SqlConnection connection =
@@ -1396,16 +1472,19 @@ public sealed class SonicSetupRepository
: display
});
command.Parameters.Add(
new SqlParameter(
"@CertificateCheckUrl",
SqlDbType.NVarChar,
500)
{
Value = checkUrl is null
? DBNull.Value
: checkUrl
});
if (hasCertificateCheckUrl)
{
command.Parameters.Add(
new SqlParameter(
"@CertificateCheckUrl",
SqlDbType.NVarChar,
500)
{
Value = checkUrl is null
? DBNull.Value
: checkUrl
});
}
command.Parameters.Add(
new SqlParameter(
@@ -1751,7 +1830,13 @@ public sealed class SonicSetupRepository
string? checkUrl =
TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl);
const string sql = """
bool hasCertificateCheckUrl =
await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync(
_connectionString,
cancellationToken);
string sql = hasCertificateCheckUrl
? """
UPDATE [dbo].[SonicContainer]
SET
[CompanyId] = @CompanyId,
@@ -1764,6 +1849,19 @@ public sealed class SonicSetupRepository
[ModifiedBy] = SUSER_SNAME()
WHERE [SonicContainerId] = @SonicContainerId
AND [IsActive] = 1;
"""
: """
UPDATE [dbo].[SonicContainer]
SET
[CompanyId] = @CompanyId,
[SonicConnectionId] = @SonicConnectionId,
[ContainerName] = @ContainerName,
[ContainerDisplayName] = @ContainerDisplayName,
[RestartTimeoutSeconds] = @RestartTimeoutSeconds,
[ModifiedDateTime] = SYSUTCDATETIME(),
[ModifiedBy] = SUSER_SNAME()
WHERE [SonicContainerId] = @SonicContainerId
AND [IsActive] = 1;
""";
await using SqlConnection connection =
@@ -1812,16 +1910,19 @@ public sealed class SonicSetupRepository
: display
});
command.Parameters.Add(
new SqlParameter(
"@CertificateCheckUrl",
SqlDbType.NVarChar,
500)
{
Value = checkUrl is null
? DBNull.Value
: checkUrl
});
if (hasCertificateCheckUrl)
{
command.Parameters.Add(
new SqlParameter(
"@CertificateCheckUrl",
SqlDbType.NVarChar,
500)
{
Value = checkUrl is null
? DBNull.Value
: checkUrl
});
}
command.Parameters.Add(
new SqlParameter(