|
- namespace Campaign_Tracker.Server.LegacyData.Schema;
-
- /// <summary>
- /// Default <see cref="ILegacySchemaCompatibilityCheck"/> 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.
- /// </summary>
- 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<LegacySchemaCheckResult> RunAsync(CancellationToken cancellationToken = default)
- {
- var live = await _inspector.GetCurrentSchemaAsync(cancellationToken).ConfigureAwait(false);
- var drifts = new List<LegacySchemaDrift>();
-
- 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<LegacyColumnDefinition> baseline,
- IReadOnlyList<LegacyColumnDefinition> live,
- List<LegacySchemaDrift> 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<string> NormalizeConstraints(IReadOnlyList<string> constraints) =>
- constraints
- .Where(c => !string.IsNullOrWhiteSpace(c))
- .Select(c => c.Trim())
- .Order(StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- private static string FormatConstraints(IReadOnlyList<string> constraints) =>
- constraints.Count == 0 ? "(none)" : string.Join(",", constraints);
- }
|