Skip to content

Commit 5d626bb

Browse files
authored
Merge branch 'main' into fix/delta-schema-perpetual-diff
2 parents 11431ac + 9587667 commit 5d626bb

File tree

5 files changed

+523
-49
lines changed

5 files changed

+523
-49
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using KustoSchemaTools.Changes;
2+
using KustoSchemaTools.Model;
3+
4+
namespace KustoSchemaTools.Tests.Changes
5+
{
6+
public class ClusterGroupingTests
7+
{
8+
[Fact]
9+
public void BuildClusterFingerprint_IdenticalChanges_ProduceSameFingerprint()
10+
{
11+
var changes1 = CreateSampleChanges("## Table1\nSome diff");
12+
var changes2 = CreateSampleChanges("## Table1\nSome diff");
13+
var comments = new List<Comment>();
14+
15+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes1, comments, true);
16+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes2, comments, true);
17+
18+
Assert.Equal(fp1, fp2);
19+
}
20+
21+
[Fact]
22+
public void BuildClusterFingerprint_DifferentMarkdown_ProduceDifferentFingerprints()
23+
{
24+
var changes1 = CreateSampleChanges("## Table1\nDiff A");
25+
var changes2 = CreateSampleChanges("## Table1\nDiff B");
26+
var comments = new List<Comment>();
27+
28+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes1, comments, true);
29+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes2, comments, true);
30+
31+
Assert.NotEqual(fp1, fp2);
32+
}
33+
34+
[Fact]
35+
public void BuildClusterFingerprint_DifferentValidity_ProduceDifferentFingerprints()
36+
{
37+
var changes = CreateSampleChanges("## Table1\nSame diff");
38+
var comments = new List<Comment>();
39+
40+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, true);
41+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, false);
42+
43+
Assert.NotEqual(fp1, fp2);
44+
}
45+
46+
[Fact]
47+
public void BuildClusterFingerprint_DifferentComments_ProduceDifferentFingerprints()
48+
{
49+
var changes = CreateSampleChanges("## Table1\nSame diff");
50+
var comments1 = new List<Comment>
51+
{
52+
new Comment { Kind = CommentKind.Warning, Text = "Warning 1", FailsRollout = false }
53+
};
54+
var comments2 = new List<Comment>
55+
{
56+
new Comment { Kind = CommentKind.Caution, Text = "Caution 1", FailsRollout = true }
57+
};
58+
59+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments1, true);
60+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments2, true);
61+
62+
Assert.NotEqual(fp1, fp2);
63+
}
64+
65+
[Fact]
66+
public void BuildClusterFingerprint_EmptyChanges_ProduceSameFingerprint()
67+
{
68+
var changes = new List<IChange>();
69+
var comments = new List<Comment>();
70+
71+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, true);
72+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, true);
73+
74+
Assert.Equal(fp1, fp2);
75+
}
76+
77+
[Fact]
78+
public void BuildClusterFingerprint_CommentsInDifferentOrder_ProduceSameFingerprint()
79+
{
80+
var changes = CreateSampleChanges("## Table1\nSame diff");
81+
var comments1 = new List<Comment>
82+
{
83+
new Comment { Kind = CommentKind.Warning, Text = "A", FailsRollout = false },
84+
new Comment { Kind = CommentKind.Note, Text = "B", FailsRollout = false }
85+
};
86+
var comments2 = new List<Comment>
87+
{
88+
new Comment { Kind = CommentKind.Note, Text = "B", FailsRollout = false },
89+
new Comment { Kind = CommentKind.Warning, Text = "A", FailsRollout = false }
90+
};
91+
92+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments1, true);
93+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments2, true);
94+
95+
Assert.Equal(fp1, fp2);
96+
}
97+
98+
private static List<IChange> CreateSampleChanges(string markdown)
99+
{
100+
return new List<IChange>
101+
{
102+
new FakeChange(markdown)
103+
};
104+
}
105+
106+
private class FakeChange : IChange
107+
{
108+
public FakeChange(string markdown)
109+
{
110+
Markdown = markdown;
111+
}
112+
113+
public string EntityType => "Test";
114+
public string Entity => "TestEntity";
115+
public List<DatabaseScriptContainer> Scripts => new List<DatabaseScriptContainer>();
116+
public string Markdown { get; }
117+
public Comment Comment { get; set; }
118+
}
119+
}
120+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using KustoSchemaTools.Changes;
2+
3+
namespace KustoSchemaTools.Tests.Changes
4+
{
5+
public class ColumnDiffHelperTests
6+
{
7+
[Fact]
8+
public void BuildColumnDiff_WithAddedColumns_ShowsAdditions()
9+
{
10+
var oldColumns = new Dictionary<string, string>
11+
{
12+
{ "id", "string" },
13+
{ "timestamp", "datetime" }
14+
};
15+
var newColumns = new Dictionary<string, string>
16+
{
17+
{ "id", "string" },
18+
{ "timestamp", "datetime" },
19+
{ "new_col", "long" }
20+
};
21+
22+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
23+
24+
Assert.NotNull(result);
25+
Assert.Contains("+ new_col: long", result);
26+
Assert.DoesNotContain("id", result);
27+
Assert.DoesNotContain("timestamp", result);
28+
}
29+
30+
[Fact]
31+
public void BuildColumnDiff_WithTypeChange_ShowsTypeChange()
32+
{
33+
var oldColumns = new Dictionary<string, string>
34+
{
35+
{ "id", "string" },
36+
{ "count", "int" }
37+
};
38+
var newColumns = new Dictionary<string, string>
39+
{
40+
{ "id", "string" },
41+
{ "count", "long" }
42+
};
43+
44+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
45+
46+
Assert.NotNull(result);
47+
Assert.Contains("! count: int → long", result);
48+
Assert.DoesNotContain("id", result);
49+
}
50+
51+
[Fact]
52+
public void BuildColumnDiff_WithRemovedColumns_ShowsInformationalNote()
53+
{
54+
var oldColumns = new Dictionary<string, string>
55+
{
56+
{ "id", "string" },
57+
{ "old_col", "string" },
58+
{ "timestamp", "datetime" }
59+
};
60+
var newColumns = new Dictionary<string, string>
61+
{
62+
{ "id", "string" },
63+
{ "timestamp", "datetime" }
64+
};
65+
66+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
67+
68+
Assert.NotNull(result);
69+
Assert.Contains("old_col: string (live only)", result);
70+
Assert.Contains(".create-merge", result);
71+
Assert.DoesNotContain("+ ", result);
72+
}
73+
74+
[Fact]
75+
public void BuildColumnDiff_WithNoChanges_ReturnsNull()
76+
{
77+
var oldColumns = new Dictionary<string, string>
78+
{
79+
{ "id", "string" },
80+
{ "timestamp", "datetime" }
81+
};
82+
var newColumns = new Dictionary<string, string>
83+
{
84+
{ "id", "string" },
85+
{ "timestamp", "datetime" }
86+
};
87+
88+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
89+
90+
Assert.Null(result);
91+
}
92+
93+
[Fact]
94+
public void BuildColumnDiff_WithNullOldColumns_TreatsAsNewTable()
95+
{
96+
var newColumns = new Dictionary<string, string>
97+
{
98+
{ "id", "string" },
99+
{ "timestamp", "datetime" }
100+
};
101+
102+
var result = ColumnDiffHelper.BuildColumnDiff(null, newColumns);
103+
104+
Assert.NotNull(result);
105+
Assert.Contains("+ id: string", result);
106+
Assert.Contains("+ timestamp: datetime", result);
107+
}
108+
109+
[Fact]
110+
public void BuildColumnDiff_WithNullNewColumns_ReturnsNull()
111+
{
112+
var oldColumns = new Dictionary<string, string>
113+
{
114+
{ "id", "string" }
115+
};
116+
117+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, null);
118+
119+
Assert.Null(result);
120+
}
121+
122+
[Fact]
123+
public void BuildColumnDiff_WithMixedChanges_ShowsAllCategories()
124+
{
125+
var oldColumns = new Dictionary<string, string>
126+
{
127+
{ "id", "string" },
128+
{ "count", "int" },
129+
{ "removed_col", "string" }
130+
};
131+
var newColumns = new Dictionary<string, string>
132+
{
133+
{ "id", "string" },
134+
{ "count", "long" },
135+
{ "new_col", "datetime" }
136+
};
137+
138+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
139+
140+
Assert.NotNull(result);
141+
Assert.Contains("+ new_col: datetime", result);
142+
Assert.Contains("! count: int → long", result);
143+
Assert.Contains("removed_col: string (live only)", result);
144+
}
145+
146+
[Fact]
147+
public void BuildColumnDiff_CaseInsensitiveTypeComparison_IgnoresCaseDifference()
148+
{
149+
var oldColumns = new Dictionary<string, string>
150+
{
151+
{ "id", "String" }
152+
};
153+
var newColumns = new Dictionary<string, string>
154+
{
155+
{ "id", "string" }
156+
};
157+
158+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
159+
160+
Assert.Null(result);
161+
}
162+
163+
[Fact]
164+
public void BuildColumnDiff_ReorderOnly_ReturnsNull()
165+
{
166+
var oldColumns = new Dictionary<string, string>
167+
{
168+
{ "id", "string" },
169+
{ "timestamp", "datetime" },
170+
{ "count", "long" }
171+
};
172+
// Same columns, different insertion order in the dictionary
173+
var newColumns = new Dictionary<string, string>
174+
{
175+
{ "count", "long" },
176+
{ "id", "string" },
177+
{ "timestamp", "datetime" }
178+
};
179+
180+
var result = ColumnDiffHelper.BuildColumnDiff(oldColumns, newColumns);
181+
182+
Assert.Null(result);
183+
}
184+
}
185+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using KustoSchemaTools.Model;
2+
using System.Text;
3+
4+
namespace KustoSchemaTools.Changes
5+
{
6+
/// <summary>
7+
/// Generates a human-readable column-level diff for table schema changes.
8+
/// Instead of showing full .create-merge table lines, shows only the columns
9+
/// that were added or had their type changed.
10+
/// </summary>
11+
public static class ColumnDiffHelper
12+
{
13+
/// <summary>
14+
/// Builds a column-level diff between two sets of columns.
15+
/// </summary>
16+
/// <param name="oldColumns">Columns from the live cluster state (may be null for new tables).</param>
17+
/// <param name="newColumns">Columns from the desired YAML state.</param>
18+
/// <returns>
19+
/// A formatted diff string showing added columns and type changes,
20+
/// or null if there are no meaningful column differences.
21+
/// </returns>
22+
public static string? BuildColumnDiff(Dictionary<string, string>? oldColumns, Dictionary<string, string>? newColumns)
23+
{
24+
if (newColumns == null) return null;
25+
oldColumns ??= new Dictionary<string, string>();
26+
27+
var added = newColumns
28+
.Where(c => !oldColumns.ContainsKey(c.Key))
29+
.ToList();
30+
31+
var typeChanged = newColumns
32+
.Where(c => oldColumns.ContainsKey(c.Key) && !string.Equals(oldColumns[c.Key], c.Value, StringComparison.OrdinalIgnoreCase))
33+
.Select(c => new { Name = c.Key, OldType = oldColumns[c.Key], NewType = c.Value })
34+
.ToList();
35+
36+
var removedFromYaml = oldColumns
37+
.Where(c => !newColumns.ContainsKey(c.Key))
38+
.ToList();
39+
40+
if (!added.Any() && !typeChanged.Any() && !removedFromYaml.Any())
41+
return null;
42+
43+
var sb = new StringBuilder();
44+
sb.AppendLine("```diff");
45+
46+
foreach (var col in added)
47+
{
48+
sb.AppendLine($"+ {col.Key}: {col.Value}");
49+
}
50+
51+
foreach (var col in typeChanged)
52+
{
53+
sb.AppendLine($"! {col.Name}: {col.OldType}{col.NewType}");
54+
}
55+
56+
if (removedFromYaml.Any())
57+
{
58+
sb.AppendLine("```");
59+
sb.AppendLine();
60+
sb.AppendLine("> **Note**: The following columns exist in the live cluster but not in YAML.");
61+
sb.AppendLine("> `.create-merge` does not remove columns — they will remain on the table.");
62+
sb.AppendLine();
63+
sb.AppendLine("```");
64+
foreach (var col in removedFromYaml)
65+
{
66+
sb.AppendLine($" {col.Key}: {col.Value} (live only)");
67+
}
68+
}
69+
70+
sb.AppendLine("```");
71+
return sb.ToString();
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)