|
1 | 1 | using System.Data; |
2 | 2 | using System.Diagnostics; |
| 3 | +using System.Globalization; |
3 | 4 | using System.Security.Cryptography; |
4 | 5 | using System.Text; |
5 | 6 | using System.Text.Json; |
@@ -819,6 +820,7 @@ public async Task<object> CompareModuleToRepoAsync( |
819 | 820 | normalizedMatch = comparison.NormalizedMatch, |
820 | 821 | sqlNormalizedMatch = comparison.SqlNormalizedMatch, |
821 | 822 | differenceKind = comparison.DifferenceKind, |
| 823 | + firstBodyDifference = comparison.FirstBodyDifference, |
822 | 824 | nextActions = ModuleCompareNextActions |
823 | 825 | }; |
824 | 826 | } |
@@ -2461,7 +2463,15 @@ private async Task<ModuleFileComparisonResult> BuildModuleFileComparisonAsync( |
2461 | 2463 | var sqlNormalizedMatch = string.Equals(dbSqlNormalized, fileSqlNormalized, StringComparison.Ordinal); |
2462 | 2464 | var differenceKind = ClassifyModuleFileDifference(exactMatch, normalizedMatch, sqlNormalizedMatch); |
2463 | 2465 | var ignoredWrapperDifferences = DetectIgnoredWrapperDifferences(module.Definition, fileText); |
2464 | | - var summary = BuildModuleFileComparisonSummary(file, diff, exactMatch, normalizedMatch, sqlNormalizedMatch, differenceKind); |
| 2466 | + var firstBodyDifference = FindFirstBodyDifference(module.Definition, fileText); |
| 2467 | + var summary = BuildModuleFileComparisonSummary( |
| 2468 | + file, |
| 2469 | + diff, |
| 2470 | + exactMatch, |
| 2471 | + normalizedMatch, |
| 2472 | + sqlNormalizedMatch, |
| 2473 | + differenceKind, |
| 2474 | + firstBodyDifference); |
2465 | 2475 |
|
2466 | 2476 | return new ModuleFileComparisonResult( |
2467 | 2477 | module.Schema, |
@@ -2490,6 +2500,7 @@ private async Task<ModuleFileComparisonResult> BuildModuleFileComparisonAsync( |
2490 | 2500 | sqlNormalizedMatch, |
2491 | 2501 | differenceKind, |
2492 | 2502 | ignoredWrapperDifferences, |
| 2503 | + firstBodyDifference, |
2493 | 2504 | summary, |
2494 | 2505 | diff, |
2495 | 2506 | ModuleCompareNextActions); |
@@ -2692,18 +2703,27 @@ private static string BuildModuleFileComparisonSummary( |
2692 | 2703 | bool exactMatch, |
2693 | 2704 | bool normalizedMatch, |
2694 | 2705 | bool sqlNormalizedMatch, |
2695 | | - string differenceKind) |
| 2706 | + string differenceKind, |
| 2707 | + ModuleBodyDifference? firstBodyDifference) |
2696 | 2708 | { |
2697 | 2709 | var returnedDiffLineCount = diff.Hunks.Sum(hunk => hunk.DatabaseLines.Length + hunk.FileLines.Length); |
2698 | 2710 | var truncatedText = diff.Truncated ? "truncated" : "not truncated"; |
2699 | | - return $"{differenceKind}; selected {file.Name}; exactMatch={FormatBool(exactMatch)}; normalizedMatch={FormatBool(normalizedMatch)}; sqlNormalizedMatch={FormatBool(sqlNormalizedMatch)}; {diff.Hunks.Length} hunks; changedLines=db:{diff.DatabaseChangedLineCount},file:{diff.FileChangedLineCount}; returnedDiffLines={returnedDiffLineCount}; {truncatedText}"; |
| 2711 | + var firstBodyText = firstBodyDifference is null |
| 2712 | + ? "firstBodyDifference=none" |
| 2713 | + : $"firstBodyDifference=body:{firstBodyDifference.BodyLine},db:{FormatNullableInt(firstBodyDifference.DatabaseLine)},file:{FormatNullableInt(firstBodyDifference.FileLine)}"; |
| 2714 | + return $"{differenceKind}; selected {file.Name}; exactMatch={FormatBool(exactMatch)}; normalizedMatch={FormatBool(normalizedMatch)}; sqlNormalizedMatch={FormatBool(sqlNormalizedMatch)}; {diff.Hunks.Length} hunks; changedLines=db:{diff.DatabaseChangedLineCount},file:{diff.FileChangedLineCount}; returnedDiffLines={returnedDiffLineCount}; {firstBodyText}; {truncatedText}"; |
2700 | 2715 | } |
2701 | 2716 |
|
2702 | 2717 | private static string FormatBool(bool value) |
2703 | 2718 | { |
2704 | 2719 | return value ? "true" : "false"; |
2705 | 2720 | } |
2706 | 2721 |
|
| 2722 | + private static string FormatNullableInt(int? value) |
| 2723 | + { |
| 2724 | + return value?.ToString(CultureInfo.InvariantCulture) ?? "none"; |
| 2725 | + } |
| 2726 | + |
2707 | 2727 | private static IEnumerable<FileInfo> EnumerateSqlFiles(DirectoryInfo root) |
2708 | 2728 | { |
2709 | 2729 | var pending = new Stack<DirectoryInfo>(); |
@@ -3387,52 +3407,83 @@ private static string NormalizeTextForComparison(string text) |
3387 | 3407 | } |
3388 | 3408 |
|
3389 | 3409 | internal static string NormalizeSqlModuleTextForComparison(string text) |
| 3410 | + { |
| 3411 | + return string.Join( |
| 3412 | + "\n", |
| 3413 | + BuildSqlModuleComparableLines(text).Select(line => line.Text)) |
| 3414 | + .Trim(); |
| 3415 | + } |
| 3416 | + |
| 3417 | + internal static ModuleBodyDifference? FindFirstBodyDifference(string databaseDefinition, string fileText) |
| 3418 | + { |
| 3419 | + var databaseLines = BuildSqlModuleComparableLines(databaseDefinition); |
| 3420 | + var fileLines = BuildSqlModuleComparableLines(fileText); |
| 3421 | + var maxLineCount = Math.Max(databaseLines.Length, fileLines.Length); |
| 3422 | + for (var i = 0; i < maxLineCount; i++) |
| 3423 | + { |
| 3424 | + var databaseLine = i < databaseLines.Length ? databaseLines[i] : null; |
| 3425 | + var fileLine = i < fileLines.Length ? fileLines[i] : null; |
| 3426 | + if (string.Equals(databaseLine?.Text, fileLine?.Text, StringComparison.Ordinal)) |
| 3427 | + { |
| 3428 | + continue; |
| 3429 | + } |
| 3430 | + |
| 3431 | + return new ModuleBodyDifference(i + 1, databaseLine?.OriginalLineNumber, fileLine?.OriginalLineNumber); |
| 3432 | + } |
| 3433 | + |
| 3434 | + return null; |
| 3435 | + } |
| 3436 | + |
| 3437 | + private static SqlModuleComparableLine[] BuildSqlModuleComparableLines(string text) |
3390 | 3438 | { |
3391 | 3439 | var lines = SplitDefinitionLines(text.Trim('\uFEFF')) |
3392 | | - .Select(line => line.TrimEnd()) |
| 3440 | + .Select((line, index) => new SqlModuleComparableLine(index + 1, line.TrimEnd())) |
3393 | 3441 | .ToList(); |
3394 | 3442 |
|
3395 | | - while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[0])) |
| 3443 | + while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[0].Text)) |
3396 | 3444 | { |
3397 | 3445 | lines.RemoveAt(0); |
3398 | 3446 | } |
3399 | 3447 |
|
3400 | | - while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[^1])) |
| 3448 | + while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[^1].Text)) |
3401 | 3449 | { |
3402 | 3450 | lines.RemoveAt(lines.Count - 1); |
3403 | 3451 | } |
3404 | 3452 |
|
3405 | 3453 | while (lines.Count > 0 |
3406 | | - && (SqlBatchSeparatorRegex.IsMatch(lines[0]) |
3407 | | - || SqlSessionSetRegex.IsMatch(lines[0]) |
3408 | | - || string.IsNullOrWhiteSpace(lines[0]))) |
| 3454 | + && (SqlBatchSeparatorRegex.IsMatch(lines[0].Text) |
| 3455 | + || SqlSessionSetRegex.IsMatch(lines[0].Text) |
| 3456 | + || string.IsNullOrWhiteSpace(lines[0].Text))) |
3409 | 3457 | { |
3410 | 3458 | lines.RemoveAt(0); |
3411 | 3459 | } |
3412 | 3460 |
|
3413 | 3461 | while (lines.Count > 0 |
3414 | | - && (SqlBatchSeparatorRegex.IsMatch(lines[^1]) |
3415 | | - || string.IsNullOrWhiteSpace(lines[^1]))) |
| 3462 | + && (SqlBatchSeparatorRegex.IsMatch(lines[^1].Text) |
| 3463 | + || string.IsNullOrWhiteSpace(lines[^1].Text))) |
3416 | 3464 | { |
3417 | 3465 | lines.RemoveAt(lines.Count - 1); |
3418 | 3466 | } |
3419 | 3467 |
|
3420 | 3468 | for (var i = 0; i < lines.Count; i++) |
3421 | 3469 | { |
3422 | | - if (string.IsNullOrWhiteSpace(lines[i])) |
| 3470 | + if (string.IsNullOrWhiteSpace(lines[i].Text)) |
3423 | 3471 | { |
3424 | 3472 | continue; |
3425 | 3473 | } |
3426 | 3474 |
|
3427 | | - lines[i] = SqlModuleCreateRegex.Replace(lines[i], match => |
| 3475 | + lines[i] = lines[i] with |
3428 | 3476 | { |
3429 | | - var objectType = Regex.Replace(match.Groups[1].Value.ToUpperInvariant(), @"\s+", " "); |
3430 | | - return $"CREATE {objectType}"; |
3431 | | - }); |
| 3477 | + Text = SqlModuleCreateRegex.Replace(lines[i].Text, match => |
| 3478 | + { |
| 3479 | + var objectType = Regex.Replace(match.Groups[1].Value.ToUpperInvariant(), @"\s+", " "); |
| 3480 | + return $"CREATE {objectType}"; |
| 3481 | + }) |
| 3482 | + }; |
3432 | 3483 | break; |
3433 | 3484 | } |
3434 | 3485 |
|
3435 | | - return string.Join("\n", lines).Trim(); |
| 3486 | + return lines.ToArray(); |
3436 | 3487 | } |
3437 | 3488 |
|
3438 | 3489 | internal static TempTableAnalysis AnalyzeTempTables(string definition) |
@@ -5288,10 +5339,16 @@ private sealed record ModuleFileComparisonResult( |
5288 | 5339 | bool SqlNormalizedMatch, |
5289 | 5340 | string DifferenceKind, |
5290 | 5341 | string[] IgnoredWrapperDifferences, |
| 5342 | + ModuleBodyDifference? FirstBodyDifference, |
5291 | 5343 | string Summary, |
5292 | 5344 | ModuleFileDiff Diff, |
5293 | 5345 | string[] NextActions); |
5294 | 5346 |
|
| 5347 | + internal sealed record ModuleBodyDifference( |
| 5348 | + int BodyLine, |
| 5349 | + int? DatabaseLine, |
| 5350 | + int? FileLine); |
| 5351 | + |
5295 | 5352 | internal sealed record ModuleSqlFileDiscovery( |
5296 | 5353 | string Root, |
5297 | 5354 | string[] Patterns, |
@@ -5332,6 +5389,8 @@ private sealed record DiffOutputOptions( |
5332 | 5389 | int MaxDiffLinesPerSide, |
5333 | 5390 | bool IncludeLines); |
5334 | 5391 |
|
| 5392 | + private sealed record SqlModuleComparableLine(int OriginalLineNumber, string Text); |
| 5393 | + |
5335 | 5394 | internal sealed record ModuleFileDiffHunk( |
5336 | 5395 | int DatabaseStartLine, |
5337 | 5396 | int DatabaseEndLine, |
|
0 commit comments