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); }