using System.Globalization; namespace Campaign_Tracker.Server.LegacyData.Schema; /// /// Parses the Access schema text dump shipped at /// Initial Documents/Access_Schema.txt into a strongly-typed /// for compatibility checking. /// /// File format (one table per block): /// /// Table: Contacts /// --------------- /// Column: ID Type: 3 Size: Nullable: False /// Column: EMAIL Type: 130 Size: 255 Nullable: True /// /// public static class LegacySchemaBaselineParser { public static LegacySchemaBaseline ParseFile(string filePath, DateTimeOffset capturedAt) { if (!File.Exists(filePath)) { throw new FileNotFoundException( $"Legacy schema baseline file not found: {filePath}", filePath); } var text = File.ReadAllText(filePath); return Parse(text, filePath, capturedAt); } public static LegacySchemaBaseline Parse(string text, string source, DateTimeOffset capturedAt) { var tables = new List(); string? currentTable = null; var currentColumns = new List(); foreach (var rawLine in text.Split('\n')) { var line = rawLine.TrimEnd('\r').TrimEnd(); if (line.Length == 0) continue; if (line.All(c => c == '-')) continue; if (line.StartsWith("Table:", StringComparison.Ordinal)) { FlushTable(tables, currentTable, currentColumns); currentTable = line["Table:".Length..].Trim(); currentColumns = new List(); continue; } var trimmed = line.TrimStart(); if (!trimmed.StartsWith("Column:", StringComparison.Ordinal)) continue; currentColumns.Add(ParseColumn(trimmed)); } FlushTable(tables, currentTable, currentColumns); return new LegacySchemaBaseline(tables, source, capturedAt); } private static void FlushTable( List sink, string? tableName, List columns) { if (string.IsNullOrWhiteSpace(tableName) || columns.Count == 0) return; sink.Add(new LegacyTableDefinition(tableName, columns.ToArray())); } private static LegacyColumnDefinition ParseColumn(string line) { // "Column: NAME Type: 130 Size: 255 Nullable: True" var name = ReadField(line, "Column:", ["Type:"]); var typeRaw = ReadField(line, "Type:", ["Size:", "Nullable:"]); var sizeRaw = ReadField(line, "Size:", ["Nullable:"]); var nullableRaw = ReadField(line, "Nullable:", []); if (!int.TryParse(typeRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var typeCode)) { throw new FormatException($"Invalid Type code in column line: {line}"); } int? size = null; if (!string.IsNullOrWhiteSpace(sizeRaw) && int.TryParse(sizeRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedSize)) { size = parsedSize; } var nullable = string.Equals(nullableRaw, "True", StringComparison.OrdinalIgnoreCase); return new LegacyColumnDefinition(name, typeCode, size, nullable); } private static string ReadField(string line, string label, string[] terminators) { var start = line.IndexOf(label, StringComparison.Ordinal); if (start < 0) return string.Empty; start += label.Length; var end = line.Length; foreach (var terminator in terminators) { var t = line.IndexOf(terminator, start, StringComparison.Ordinal); if (t >= 0 && t < end) end = t; } return line[start..end].Trim(); } }