Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3163.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3163
affected:
- src/CodeIndex/Cli/DiffCommandRunner.cs
- tests/CodeIndex.Tests/DiffCommandRunnerTests.cs
---

## English

- **Detailed diff row output now caps large field samples (#3163)** - `cdidx diff --detailed` now truncates oversized encoded row fields with the original length and a stable SHA-256 fingerprint, so large signatures or contexts do not inflate diff reports while full-value comparison still detects drift.

## 日本語

- **詳細 diff の行出力で大きなフィールドサンプルを上限付きにしました (#3163)** - `cdidx diff --detailed` は巨大な encoded row フィールドを元の長さと安定した SHA-256 fingerprint 付きで切り詰めるため、大きな signature や context が diff レポートを肥大化させず、full value 比較による drift 検出は維持されます。
71 changes: 45 additions & 26 deletions src/CodeIndex/Cli/DiffCommandRunner.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CodeIndex.Database;
using CodeIndex.Indexer;
Expand All @@ -8,6 +10,7 @@ namespace CodeIndex.Cli;
public static class DiffCommandRunner
{
private const int DefaultDiffLimit = 20;
internal const int MaxDiffEncodedFieldSampleLength = 1024;
internal static int MaxDiffLimit => QueryCommandRunner.NumericFlagUpperBounds["--limit"];
private const int DriftExitCode = 1;
private const int SchemaMismatchExitCode = 2;
Expand Down Expand Up @@ -426,8 +429,8 @@ private static OrderedRowsDiff DiffOrderedRows(SqliteConnection leftConnection,

var onlyInLeft = new List<string>(limit);
var onlyInRight = new List<string>(limit);
var leftHasValue = TryReadEncodedRow(leftReader, out var leftValue);
var rightHasValue = TryReadEncodedRow(rightReader, out var rightValue);
var leftHasValue = TryReadRow(leftReader, out var leftValue);
var rightHasValue = TryReadRow(rightReader, out var rightValue);
var equal = true;

while (leftHasValue || rightHasValue)
Expand All @@ -438,23 +441,23 @@ private static OrderedRowsDiff DiffOrderedRows(SqliteConnection leftConnection,

if (comparison == 0)
{
leftHasValue = TryReadEncodedRow(leftReader, out leftValue);
rightHasValue = TryReadEncodedRow(rightReader, out rightValue);
leftHasValue = TryReadRow(leftReader, out leftValue);
rightHasValue = TryReadRow(rightReader, out rightValue);
continue;
}

equal = false;
if (comparison < 0)
{
if (onlyInLeft.Count < limit)
onlyInLeft.Add(leftValue.Encoded);
leftHasValue = TryReadEncodedRow(leftReader, out leftValue);
onlyInLeft.Add(EncodeRow(leftValue.SortValues));
leftHasValue = TryReadRow(leftReader, out leftValue);
}
else
{
if (onlyInRight.Count < limit)
onlyInRight.Add(rightValue.Encoded);
rightHasValue = TryReadEncodedRow(rightReader, out rightValue);
onlyInRight.Add(EncodeRow(rightValue.SortValues));
rightHasValue = TryReadRow(rightReader, out rightValue);
}

if (onlyInLeft.Count >= limit && onlyInRight.Count >= limit)
Expand Down Expand Up @@ -525,14 +528,14 @@ private static bool RowsEqual(SqliteConnection leftConnection, SqliteConnection
using var leftReader = leftCommand.ExecuteReader();
using var rightReader = rightCommand.ExecuteReader();

var leftHasValue = TryReadEncodedRow(leftReader, out var leftValue);
var rightHasValue = TryReadEncodedRow(rightReader, out var rightValue);
var leftHasValue = TryReadRow(leftReader, out var leftValue);
var rightHasValue = TryReadRow(rightReader, out var rightValue);
while (leftHasValue && rightHasValue)
{
if (!string.Equals(leftValue.Encoded, rightValue.Encoded, StringComparison.Ordinal))
if (CompareRows(leftValue, rightValue) != 0)
return false;
leftHasValue = TryReadEncodedRow(leftReader, out leftValue);
rightHasValue = TryReadEncodedRow(rightReader, out rightValue);
leftHasValue = TryReadRow(leftReader, out leftValue);
rightHasValue = TryReadRow(rightReader, out rightValue);
}

return leftHasValue == rightHasValue;
Expand Down Expand Up @@ -560,23 +563,23 @@ private static bool StringRowsEqual(SqliteConnection leftConnection, SqliteConne
return leftHasValue == rightHasValue;
}

private static bool TryReadEncodedRow(SqliteDataReader reader, out EncodedDiffRow value)
private static bool TryReadRow(SqliteDataReader reader, out DiffRow value)
{
if (!reader.Read())
{
value = EncodedDiffRow.Empty;
value = DiffRow.Empty;
return false;
}

var sortValues = new object?[reader.FieldCount];
for (var i = 0; i < reader.FieldCount; i++)
sortValues[i] = reader.IsDBNull(i) ? null : reader.GetValue(i);

value = new EncodedDiffRow(EncodeRow(reader), sortValues);
value = new DiffRow(sortValues);
return true;
}

private static int CompareRows(EncodedDiffRow left, EncodedDiffRow right)
private static int CompareRows(DiffRow left, DiffRow right)
{
var count = Math.Min(left.SortValues.Length, right.SortValues.Length);
for (var i = 0; i < count; i++)
Expand Down Expand Up @@ -698,24 +701,41 @@ private static long ExecuteCountIfTableExists(SqliteConnection connection, strin
return Convert.ToInt64(command.ExecuteScalar(), System.Globalization.CultureInfo.InvariantCulture);
}

private static string EncodeRow(SqliteDataReader reader)
private static string EncodeRow(object?[] values)
{
var fields = new string[reader.FieldCount];
for (var i = 0; i < reader.FieldCount; i++)
var fields = new string[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (reader.IsDBNull(i))
var rawValue = values[i];
if (rawValue is null or DBNull)
{
fields[i] = "-1:";
continue;
}

var value = Convert.ToString(reader.GetValue(i), System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;
fields[i] = value.Length.ToString(System.Globalization.CultureInfo.InvariantCulture) + ":" + value;
var value = Convert.ToString(rawValue, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;
var encodedValue = EncodeFieldValue(value);
fields[i] = encodedValue.Length.ToString(System.Globalization.CultureInfo.InvariantCulture) + ":" + encodedValue;
}

return string.Join("|", fields);
}

private static string EncodeFieldValue(string value)
{
if (value.Length <= MaxDiffEncodedFieldSampleLength)
return value;

var sample = value[..MaxDiffEncodedFieldSampleLength];
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
return sample
+ "...[truncated original_length="
+ value.Length.ToString(System.Globalization.CultureInfo.InvariantCulture)
+ " sha256="
+ hash
+ "]";
}

private static void WriteJson(DiffJsonResult result, JsonSerializerOptions jsonOptions)
{
Console.WriteLine(JsonSerializer.Serialize(
Expand Down Expand Up @@ -791,11 +811,10 @@ private sealed record OrderedRowsDiff(
List<string> OnlyInLeft,
List<string> OnlyInRight);

private sealed record EncodedDiffRow(
string Encoded,
private sealed record DiffRow(
object?[] SortValues)
{
public static readonly EncodedDiffRow Empty = new(string.Empty, []);
public static readonly DiffRow Empty = new([]);
}

private sealed record DiffDbHeader(
Expand Down
67 changes: 67 additions & 0 deletions tests/CodeIndex.Tests/DiffCommandRunnerTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using CodeIndex.Cli;
using CodeIndex.Database;
Expand Down Expand Up @@ -179,6 +181,71 @@ public void Run_DetailedJsonReportsLimitedSymbolRows_Issue2885()
}
}

[Fact]
public void Run_DetailedJsonTruncatesLargeEncodedSymbolFields_Issue3163()
{
var leftRoot = TestProjectHelper.CreateTempProject("cdidx_diff_large_field_left");
var rightRoot = TestProjectHelper.CreateTempProject("cdidx_diff_large_field_right");
try
{
var leftDb = TestProjectHelper.CreateProjectDb(leftRoot);
var rightDb = TestProjectHelper.CreateProjectDb(rightRoot);
TestProjectHelper.InsertIndexedFile(leftDb, "src/Same.cs", "csharp", "public class Same { }");
TestProjectHelper.InsertIndexedFile(rightDb, "src/Same.cs", "csharp", "public class Same { }");

var longSignature = new string('a', DiffCommandRunner.MaxDiffEncodedFieldSampleLength * 4);
InsertSyntheticMethodSymbol(leftDb, "src/Same.cs", "Drifted", longSignature);

var (exitCode, output) = RunWithCapturedOut([leftDb, rightDb, "--json", "--detailed", "--limit", "1"]);

Assert.Equal(1, exitCode);
using var document = JsonDocument.Parse(output);
var row = Assert.Single(document.RootElement.GetProperty("symbols_only_in_left").EnumerateArray()).GetString();
Assert.NotNull(row);
var expectedHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(longSignature))).ToLowerInvariant();
Assert.Contains(new string('a', DiffCommandRunner.MaxDiffEncodedFieldSampleLength), row, StringComparison.Ordinal);
Assert.Contains($"[truncated original_length={longSignature.Length}", row, StringComparison.Ordinal);
Assert.Contains($"sha256={expectedHash}", row, StringComparison.Ordinal);
Assert.DoesNotContain(new string('a', DiffCommandRunner.MaxDiffEncodedFieldSampleLength + 1), row, StringComparison.Ordinal);
Assert.True(row!.Length < longSignature.Length);
}
finally
{
TestProjectHelper.DeleteDirectory(leftRoot);
TestProjectHelper.DeleteDirectory(rightRoot);
}
}

[Fact]
public void Run_SummaryOnlyDetectsLargeRowDriftAfterSharedDisplayPrefix_Issue3163()
{
var leftRoot = TestProjectHelper.CreateTempProject("cdidx_diff_large_prefix_left");
var rightRoot = TestProjectHelper.CreateTempProject("cdidx_diff_large_prefix_right");
try
{
var leftDb = TestProjectHelper.CreateProjectDb(leftRoot);
var rightDb = TestProjectHelper.CreateProjectDb(rightRoot);
TestProjectHelper.InsertIndexedFile(leftDb, "src/Same.cs", "csharp", "public class Same { public void Run() { } }");
TestProjectHelper.InsertIndexedFile(rightDb, "src/Same.cs", "csharp", "public class Same { public void Run() { } }");

var sharedPrefix = new string('x', DiffCommandRunner.MaxDiffEncodedFieldSampleLength);
UpdateFirstChunkContent(leftDb, sharedPrefix + "left");
UpdateFirstChunkContent(rightDb, sharedPrefix + "right");

var (exitCode, output) = RunWithCapturedOut([leftDb, rightDb, "--summary-only"]);

Assert.Equal(1, exitCode);
using var document = JsonDocument.Parse(output);
Assert.Equal("different", document.RootElement.GetProperty("status").GetString());
Assert.False(document.RootElement.GetProperty("identical").GetBoolean());
}
finally
{
TestProjectHelper.DeleteDirectory(leftRoot);
TestProjectHelper.DeleteDirectory(rightRoot);
}
}

[Fact]
public void Run_DetailedJsonUsesSqlOrderForStreamingSymbolDiff_Issue2885()
{
Expand Down
Loading