namespace Campaign_Tracker.Server.LegacyData.Schema;
///
/// Default implementation.
///
/// Compares the baseline (captured at initialization) against the current
/// schema returned by the inspector. Reports drift entries for:
/// - tables present in baseline but missing in current (TableMissing)
/// - tables present in current but missing in baseline (TableAdded)
/// - columns present in baseline but missing in current (ColumnMissing)
/// - columns present in current but missing in baseline (ColumnAdded)
/// - columns present in both with different type/size/nullability
/// Comparison is case-insensitive on table and column names to match Access
/// behavior.
///
public sealed class LegacySchemaCompatibilityCheck : ILegacySchemaCompatibilityCheck
{
private readonly LegacySchemaBaseline _baseline;
private readonly ILegacySchemaInspector _inspector;
private readonly TimeProvider _timeProvider;
public LegacySchemaCompatibilityCheck(
LegacySchemaBaseline baseline,
ILegacySchemaInspector inspector,
TimeProvider? timeProvider = null)
{
_baseline = baseline ?? throw new ArgumentNullException(nameof(baseline));
_inspector = inspector ?? throw new ArgumentNullException(nameof(inspector));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task RunAsync(CancellationToken cancellationToken = default)
{
var live = await _inspector.GetCurrentSchemaAsync(cancellationToken).ConfigureAwait(false);
var drifts = new List();
var baselineByName = _baseline.Tables
.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
var liveByName = live
.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
foreach (var (name, baselineTable) in baselineByName)
{
if (!liveByName.TryGetValue(name, out var liveTable))
{
drifts.Add(new LegacySchemaDrift(
name, null, LegacySchemaChangeType.TableMissing,
$"Table '{name}' is in the approved baseline but missing from the live database."));
continue;
}
CompareColumns(name, baselineTable.Columns, liveTable.Columns, drifts);
}
foreach (var (name, _) in liveByName)
{
if (!baselineByName.ContainsKey(name))
{
drifts.Add(new LegacySchemaDrift(
name, null, LegacySchemaChangeType.TableAdded,
$"Table '{name}' exists in the live database but is not part of the approved baseline."));
}
}
return new LegacySchemaCheckResult(
Passed: drifts.Count == 0,
TablesVerified: baselineByName.Count,
DriftCount: drifts.Count,
CheckedAt: _timeProvider.GetUtcNow(),
Drifts: drifts,
BaselineSource: _baseline.Source);
}
private static void CompareColumns(
string tableName,
IReadOnlyList baseline,
IReadOnlyList live,
List sink)
{
var baselineByName = baseline.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase);
var liveByName = live.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase);
foreach (var (name, baseCol) in baselineByName)
{
if (!liveByName.TryGetValue(name, out var liveCol))
{
sink.Add(new LegacySchemaDrift(
tableName, name, LegacySchemaChangeType.ColumnMissing,
$"Column '{tableName}.{name}' is in the approved baseline but missing from the live database."));
continue;
}
if (baseCol.TypeCode != liveCol.TypeCode)
{
sink.Add(new LegacySchemaDrift(
tableName, name, LegacySchemaChangeType.ColumnTypeChanged,
$"Column '{tableName}.{name}' type changed: baseline={baseCol.TypeCode}, live={liveCol.TypeCode}."));
}
if (baseCol.Size != liveCol.Size)
{
sink.Add(new LegacySchemaDrift(
tableName, name, LegacySchemaChangeType.ColumnSizeChanged,
$"Column '{tableName}.{name}' size changed: baseline={Format(baseCol.Size)}, live={Format(liveCol.Size)}."));
}
if (baseCol.Nullable != liveCol.Nullable)
{
sink.Add(new LegacySchemaDrift(
tableName, name, LegacySchemaChangeType.ColumnNullabilityChanged,
$"Column '{tableName}.{name}' nullability changed: baseline={baseCol.Nullable}, live={liveCol.Nullable}."));
}
var baselineConstraints = NormalizeConstraints(baseCol.Constraints);
var liveConstraints = NormalizeConstraints(liveCol.Constraints);
if (!baselineConstraints.SequenceEqual(liveConstraints, StringComparer.OrdinalIgnoreCase))
{
sink.Add(new LegacySchemaDrift(
tableName, name, LegacySchemaChangeType.ColumnConstraintsChanged,
$"Column '{tableName}.{name}' constraints changed: baseline={FormatConstraints(baselineConstraints)}, live={FormatConstraints(liveConstraints)}."));
}
}
foreach (var (name, _) in liveByName)
{
if (!baselineByName.ContainsKey(name))
{
sink.Add(new LegacySchemaDrift(
tableName, name, LegacySchemaChangeType.ColumnAdded,
$"Column '{tableName}.{name}' exists in the live database but is not part of the approved baseline."));
}
}
}
private static string Format(int? value) => value?.ToString() ?? "(none)";
private static IReadOnlyList NormalizeConstraints(IReadOnlyList constraints) =>
constraints
.Where(c => !string.IsNullOrWhiteSpace(c))
.Select(c => c.Trim())
.Order(StringComparer.OrdinalIgnoreCase)
.ToArray();
private static string FormatConstraints(IReadOnlyList constraints) =>
constraints.Count == 0 ? "(none)" : string.Join(",", constraints);
}