using System; using System.Collections.Generic; using System.Text.RegularExpressions; using SyncEngine.Configuration; namespace SyncEngine.SqlServer { internal enum UniqueKeyCollisionAction { FailDuplicate, StaleUpdateOccupying, Remap, Recycle } /// /// Pure classification for unique secondary-key collisions (e.g. tblProduct.strCode). /// No database I/O — extra SQL/Access traffic happens only in UniqueKeyCollisionResolver after error 2601. /// internal static class UniqueKeyCollisionClassifier { public const double SimilarityThreshold = 0.90; public const string RemovedSuffix = "_removed"; private static readonly Regex CollapseWhitespace = new Regex(@"\s+", RegexOptions.Compiled); public static UniqueKeyCollisionAction Classify( bool occupyingPkExistsInAccess, string occupyingAccessStrCode, string incomingStrCode, string occupyingSqlDescription, string incomingDescription) { if (occupyingPkExistsInAccess) { if (UniqueValuesEqual(occupyingAccessStrCode, incomingStrCode)) { return UniqueKeyCollisionAction.FailDuplicate; } return UniqueKeyCollisionAction.StaleUpdateOccupying; } if (IsSameProduct(occupyingSqlDescription, incomingDescription)) { return UniqueKeyCollisionAction.Remap; } return UniqueKeyCollisionAction.Recycle; } public static bool IsUniqueIndexViolation(int sqlErrorNumber) { return sqlErrorNumber == 2601 || sqlErrorNumber == 2627; } public static string BuildRemovedCode(string original, int maxLength, string occupyingPk) { var code = original ?? string.Empty; if (maxLength <= 0) { return string.Empty; } if (code.Length + RemovedSuffix.Length <= maxLength) { return code + RemovedSuffix; } var keep = maxLength - RemovedSuffix.Length; if (keep > 0) { return code.Substring(0, Math.Min(keep, code.Length)) + RemovedSuffix; } return BuildPkFallback(maxLength, occupyingPk); } public static string BuildPkFallback(int maxLength, string occupyingPk) { var suffix = "_r" + (occupyingPk ?? string.Empty); if (suffix.Length <= maxLength) { return suffix; } return suffix.Substring(0, maxLength); } public static string BuildRefColumnName(string primaryKeyColumn) { return "ref" + (primaryKeyColumn ?? string.Empty); } public static IList UniqueNonPrimaryIndexes(IEnumerable indexes, string tableName) { var result = new List(); if (indexes == null || string.IsNullOrWhiteSpace(tableName)) { return result; } foreach (var index in indexes) { if (index == null || !string.Equals(index.TableName, tableName, StringComparison.OrdinalIgnoreCase) || index.IsPrimary || !index.IsUnique || index.Columns == null || index.Columns.Count != 1) { continue; } result.Add(index); } return result; } public static string NormalizeDescription(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } return CollapseWhitespace.Replace(value.Trim(), " ").ToUpperInvariant(); } internal static bool UniqueValuesEqual(string left, string right) { return string.Equals( (left ?? string.Empty).Trim(), (right ?? string.Empty).Trim(), StringComparison.OrdinalIgnoreCase); } internal static bool IsSameProduct(string occupyingSqlDescription, string incomingDescription) { var left = NormalizeDescription(occupyingSqlDescription); var right = NormalizeDescription(incomingDescription); if (left.Length == 0 || right.Length == 0) { return true; } if (string.Equals(left, right, StringComparison.Ordinal)) { return true; } return Similarity(left, right) >= SimilarityThreshold; } internal static double Similarity(string left, string right) { if (left == null) { left = string.Empty; } if (right == null) { right = string.Empty; } var max = Math.Max(left.Length, right.Length); if (max == 0) { return 1.0; } return 1.0 - ((double)Levenshtein(left, right) / max); } private static int Levenshtein(string left, string right) { var n = left.Length; var m = right.Length; var prev = new int[m + 1]; var curr = new int[m + 1]; for (var j = 0; j <= m; j++) { prev[j] = j; } for (var i = 1; i <= n; i++) { curr[0] = i; for (var j = 1; j <= m; j++) { var cost = left[i - 1] == right[j - 1] ? 0 : 1; curr[j] = Math.Min( Math.Min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); } var swap = prev; prev = curr; curr = swap; } return prev[m]; } } }