Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions KustoSchemaTools/Changes/ClusterChanges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static ClusterChangeSet GenerateChanges(Cluster oldCluster, Cluster newCl
// Run Kusto code diagnostics
foreach (var script in changeSet.Scripts)
{
var code = KustoCode.Parse(script.Text);
var code = KustoCode.Parse(script.Script.Text);
var diagnostics = code.GetDiagnostics();
script.IsValid = !diagnostics.Any();
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnostics should be captured in the Diagnostics property for consistency with ScriptCompareChange. Currently, diagnostics are only used to set IsValid but not stored. Consider adding:

script.Diagnostics = diagnostics.Any()
    ? diagnostics.Select(diagnostic => new ScriptDiagnostic
    {
        Start = diagnostic.Start,
        End = diagnostic.End,
        Description = diagnostic.Description
    }).ToList()
    : null;
Suggested change
script.IsValid = !diagnostics.Any();
script.IsValid = !diagnostics.Any();
script.Diagnostics = diagnostics.Any()
? diagnostics.Select(diagnostic => new ScriptDiagnostic
{
Start = diagnostic.Start,
End = diagnostic.End,
Description = diagnostic.Description
}).ToList()
: null;

Copilot uses AI. Check for mistakes.
}
Expand Down Expand Up @@ -244,7 +244,7 @@ private static string GenerateClusterMarkdown(ClusterChangeSet changeSet)
sb.AppendLine("```kql");
foreach (var script in changeSet.Scripts)
{
sb.AppendLine(script.Text);
sb.AppendLine(script.Script.Text);
sb.AppendLine();
}
sb.AppendLine("```");
Expand Down
2 changes: 1 addition & 1 deletion KustoSchemaTools/Changes/DatabaseChanges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ .. GenerateFollowerCachingChanges(oldState, newState, db => db.MaterializedViews

foreach(var script in result.SelectMany(itm => itm.Scripts))
{
var code = KustoCode.Parse(script.Text);
var code = KustoCode.Parse(script.Script.Text);
var diagnostics = code.GetDiagnostics();
script.IsValid = diagnostics.Any() == false;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnostics should be captured in the Diagnostics property for consistency with ScriptCompareChange. Currently, diagnostics are only used to set IsValid but not stored. Consider adding:

script.Diagnostics = diagnostics.Any()
    ? diagnostics.Select(diagnostic => new ScriptDiagnostic
    {
        Start = diagnostic.Start,
        End = diagnostic.End,
        Description = diagnostic.Description
    }).ToList()
    : null;
Suggested change
script.IsValid = diagnostics.Any() == false;
script.IsValid = diagnostics.Any() == false;
script.Diagnostics = diagnostics.Any()
? diagnostics.Select(diagnostic => new ScriptDiagnostic
{
Start = diagnostic.Start,
End = diagnostic.End,
Description = diagnostic.Description
}).ToList()
: null;

Copilot uses AI. Check for mistakes.
}
Expand Down
20 changes: 15 additions & 5 deletions KustoSchemaTools/Changes/DatabaseScriptContainer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using KustoSchemaTools.Model;
using System.Collections.Generic;
using KustoSchemaTools.Model;
using Newtonsoft.Json;

namespace KustoSchemaTools.Changes
{
Expand All @@ -23,11 +25,19 @@ public DatabaseScriptContainer(string kind, int order, string script, bool isAsy
IsAsync = isAsync;
}

[JsonProperty("script")]
public DatabaseScript Script { get; set; }
public string Kind{ get; set; }

[JsonProperty("kind")]
public string Kind { get; set; }

[JsonProperty("isValid")]
public bool? IsValid { get; set; }
public string Text => Script.Text;
public int Order => Script.Order;
public bool IsAsync { get;set; }

[JsonProperty("isAsync")]
public bool IsAsync { get; set; }

[JsonProperty("diagnostics", NullValueHandling = NullValueHandling.Ignore)]
public List<ScriptDiagnostic>? Diagnostics { get; set; }
}
}
2 changes: 1 addition & 1 deletion KustoSchemaTools/Changes/DeletionChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public List<DatabaseScriptContainer> Scripts
get
{
var sc = new DatabaseScriptContainer("Deletion", 0, $".drop {EntityType} {Entity}");
var code = KustoCode.Parse(sc.Text);
var code = KustoCode.Parse(sc.Script.Text);
var diagnostics = code.GetDiagnostics();
sc.IsValid = diagnostics.Any() == false;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnostics should be captured in the Diagnostics property for consistency with ScriptCompareChange. Currently, diagnostics are only used to set IsValid but not stored. Consider adding:

sc.Diagnostics = diagnostics.Any()
    ? diagnostics.Select(diagnostic => new ScriptDiagnostic
    {
        Start = diagnostic.Start,
        End = diagnostic.End,
        Description = diagnostic.Description
    }).ToList()
    : null;
Suggested change
sc.IsValid = diagnostics.Any() == false;
sc.IsValid = diagnostics.Any() == false;
sc.Diagnostics = diagnostics.Any()
? diagnostics.Select(diagnostic => new ScriptDiagnostic
{
Start = diagnostic.Start,
End = diagnostic.End,
Description = diagnostic.Description
}).ToList()
: null;

Copilot uses AI. Check for mistakes.
return new List<DatabaseScriptContainer> { sc };
Expand Down
2 changes: 1 addition & 1 deletion KustoSchemaTools/Changes/EntityGroupChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private void Init()
}

Scripts.Add(toScript);
var code = KustoCode.Parse(toScript.Text);
var code = KustoCode.Parse(toScript.Script.Text);
var diagnostics = code.GetDiagnostics();
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnostics should be captured in the Diagnostics property for consistency with ScriptCompareChange. Currently, diagnostics are only used to set IsValid but not stored. Consider adding:

toScript.Diagnostics = diagnostics.Any()
    ? diagnostics.Select(diagnostic => new ScriptDiagnostic
    {
        Start = diagnostic.Start,
        End = diagnostic.End,
        Description = diagnostic.Description
    }).ToList()
    : null;
Suggested change
var diagnostics = code.GetDiagnostics();
var diagnostics = code.GetDiagnostics();
toScript.Diagnostics = diagnostics.Any()
? diagnostics.Select(diagnostic => new ScriptDiagnostic
{
Start = diagnostic.Start,
End = diagnostic.End,
Description = diagnostic.Description
}).ToList()
: null;

Copilot uses AI. Check for mistakes.
toScript.IsValid = diagnostics.Any() == false;
var logo = toScript.IsValid.Value ? ":green_circle:" : ":red_circle:";
Expand Down
20 changes: 15 additions & 5 deletions KustoSchemaTools/Changes/ScriptCompareChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
using System.Data;
using DiffPlex.DiffBuilder.Model;
using Kusto.Language.Editor;
using System.Linq;

namespace KustoSchemaTools.Changes
{
public class ScriptCompareChange : BaseChange<IKustoBaseEntity>
{
public ScriptCompareChange(string entity, IKustoBaseEntity? from, IKustoBaseEntity to) : base(to.GetType().Name, entity, from, to)

Check warning on line 16 in KustoSchemaTools/Changes/ScriptCompareChange.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'from' in 'BaseChange<IKustoBaseEntity>.BaseChange(string entityType, string entity, IKustoBaseEntity from, IKustoBaseEntity to)'.
{
Init();
}
Expand All @@ -32,8 +33,8 @@
foreach (var change in to)
{
var before = from.ContainsKey(change.Kind) ? from[change.Kind] : null;
var beforeText = before?.Text ?? "";
var afterText = change.Text;
var beforeText = before?.Script.Text ?? string.Empty;
var afterText = change.Script.Text;

var singleLinebeforeText = new KustoCodeService(KustoCode.Parse(beforeText)).GetMinimalText(MinimalTextKind.SingleLine);
var singleLineafterText = new KustoCodeService(KustoCode.Parse(afterText)).GetMinimalText(MinimalTextKind.SingleLine);
Expand All @@ -51,10 +52,19 @@
var diff = InlineDiffBuilder.Diff(beforeText, afterText, true);
if (diff.Lines.All(itm => itm.Type == ChangeType.Unchanged)) continue;

var code = KustoCode.Parse(change.Text);
var code = KustoCode.Parse(change.Script.Text);

var diagnostics = code.GetDiagnostics();
change.IsValid = diagnostics.Any() == false || change.Order == -1;
var hasDiagnostics = diagnostics.Any();
change.IsValid = hasDiagnostics == false || change.Script.Order == -1;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression 'A == false' can be simplified to '!A'.

Suggested change
change.IsValid = hasDiagnostics == false || change.Script.Order == -1;
change.IsValid = !hasDiagnostics || change.Script.Order == -1;

Copilot uses AI. Check for mistakes.
change.Diagnostics = hasDiagnostics
? diagnostics.Select(diagnostic => new ScriptDiagnostic
{
Start = diagnostic.Start,
End = diagnostic.End,
Description = diagnostic.Description
}).ToList()
: null;
Scripts.Add(change);


Expand Down Expand Up @@ -102,7 +112,7 @@
}
sb.AppendLine("<tr>");
sb.AppendLine($" <td colspan=\"2\">Script:</td>");
sb.AppendLine($" <td colspan=\"10\"><pre lang=\"kql\">{change.Text.PrettifyKql()}</pre></td>");
sb.AppendLine($" <td colspan=\"10\"><pre lang=\"kql\">{change.Script.Text.PrettifyKql()}</pre></td>");
sb.AppendLine("</tr>");

if (change.IsValid == false)
Expand Down
225 changes: 225 additions & 0 deletions KustoSchemaTools/Changes/StructuredChangeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DiffPlex;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using KustoSchemaTools.Model;

namespace KustoSchemaTools.Changes
{
public static class StructuredChangeExtensions
{
public static StructuredChange ToStructuredChange(this IChange change)
{
if (change == null)
{
throw new ArgumentNullException(nameof(change));
}

var structuredChange = new StructuredChange
{
EntityType = change.EntityType,
Entity = change.Entity,
Scripts = change.Scripts?.Select(CloneScript).ToList() ?? new List<DatabaseScriptContainer>(),
Comment = StructuredComment.From(change.Comment)
};

switch (change)
{
case Heading heading:
structuredChange.ChangeType = "Heading";
structuredChange.HeadingText = heading.Entity;
structuredChange.Scripts.Clear();
break;
case DeletionChange deletion:
structuredChange.ChangeType = "Delete";
structuredChange.DeletedEntities = new List<string> { deletion.Entity };
break;
case ScriptCompareChange scriptCompare:
structuredChange.ChangeType = scriptCompare.From == null ? "Create" : "Update";
structuredChange.ScriptComparison = scriptCompare.ToStructuredScriptComparison();
structuredChange.DiffMarkdown = BuildDiffMarkdown(scriptCompare);
break;
default:
structuredChange.ChangeType = "Update";
break;
}

structuredChange.DeletedEntities ??= new List<string>();

return structuredChange;
}

private static StructuredScriptComparison? ToStructuredScriptComparison(this ScriptCompareChange change)
{
var comparison = new StructuredScriptComparison
{
NewScripts = change.Scripts?.Select(CloneScript).ToList() ?? new List<DatabaseScriptContainer>()
};

if (change.From != null)
{
var previousScripts = change.From
.CreateScripts(change.Entity, false)
.GroupBy(script => script.Kind)
.Select(group => group.First())
.ToDictionary(script => script.Kind, script => script);

foreach (var script in comparison.NewScripts)
{
if (previousScripts.TryGetValue(script.Kind, out var previous))
{
comparison.OldScripts.Add(CloneScript(previous));
}
Comment on lines +63 to +68
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Suggested change
foreach (var script in comparison.NewScripts)
{
if (previousScripts.TryGetValue(script.Kind, out var previous))
{
comparison.OldScripts.Add(CloneScript(previous));
}
foreach (var script in comparison.NewScripts.Where(s => previousScripts.ContainsKey(s.Kind)))
{
var previous = previousScripts[script.Kind];
comparison.OldScripts.Add(CloneScript(previous));

Copilot uses AI. Check for mistakes.
}
Comment on lines +63 to +69

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

var validationPayload = BuildValidationPayload(previousScripts, comparison.NewScripts);
if (validationPayload.Count > 0)
{
comparison.ValidationResults = validationPayload;
}
}

foreach (var script in comparison.OldScripts.Where(s => !s.IsValid.HasValue))
{
script.IsValid = true;
}

return comparison;
}

private static DatabaseScriptContainer CloneScript(DatabaseScriptContainer source)
{
var clone = new DatabaseScriptContainer(new DatabaseScript(source.Script.Text, source.Script.Order), source.Kind, source.IsAsync)
{
IsValid = source.IsValid
};

if (source.Diagnostics != null && source.Diagnostics.Count > 0)
{
clone.Diagnostics = source.Diagnostics
.Select(diagnostic => new ScriptDiagnostic
{
Start = diagnostic.Start,
End = diagnostic.End,
Description = diagnostic.Description
})
.ToList();
}

return clone;
}

private static Dictionary<string, object?> BuildValidationPayload(Dictionary<string, DatabaseScriptContainer> previousScripts, List<DatabaseScriptContainer> newScripts)
{
var payload = new Dictionary<string, object?>();
foreach (var script in newScripts)
{
var oldScript = previousScripts.ContainsKey(script.Kind) ? previousScripts[script.Kind] : null;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient use of 'ContainsKey' and indexer.

Suggested change
var oldScript = previousScripts.ContainsKey(script.Kind) ? previousScripts[script.Kind] : null;
previousScripts.TryGetValue(script.Kind, out var oldScript);

Copilot uses AI. Check for mistakes.
var diffPreview = BuildDiffPreview(oldScript, script);
if (diffPreview.Count > 0)
{
var keyName = string.IsNullOrWhiteSpace(script.Kind) ? "diff" : $"diff::{script.Kind}";
payload[keyName] = diffPreview;
}
}

return payload;
}

private static List<string> BuildDiffPreview(DatabaseScriptContainer? oldScript, DatabaseScriptContainer? newScript)
{
var before = GetScriptText(oldScript);
var after = GetScriptText(newScript);

if (string.Equals(before, after, StringComparison.Ordinal))
{
return new List<string>();
}

var differ = new Differ();

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This assignment to
differ
is useless, since its value is never read.

Copilot Autofix

AI 5 days ago

To fix the issue, simply remove the assignment and local declaration for the unused variable differ. In BuildDiffPreview, eliminate the line var differ = new Differ(); entirely. No other changes are required; the InlineDiffBuilder.Diff invocation works independently and does not depend on Differ. This change should be limited to the relevant lines in the method body and requires no additional imports or method definitions.

Suggested changeset 1
KustoSchemaTools/Changes/StructuredChangeExtensions.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/KustoSchemaTools/Changes/StructuredChangeExtensions.cs b/KustoSchemaTools/Changes/StructuredChangeExtensions.cs
--- a/KustoSchemaTools/Changes/StructuredChangeExtensions.cs
+++ b/KustoSchemaTools/Changes/StructuredChangeExtensions.cs
@@ -132,7 +132,6 @@
                 return new List<string>();
             }
 
-            var differ = new Differ();
             var diff = InlineDiffBuilder.Diff(before, after, false);
 
             var preview = diff.Lines
EOF
@@ -132,7 +132,6 @@
return new List<string>();
}

var differ = new Differ();
var diff = InlineDiffBuilder.Diff(before, after, false);

var preview = diff.Lines
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The differ variable is instantiated but never used. The InlineDiffBuilder.Diff method on line 143 doesn't require this variable. Consider removing:

var differ = new Differ();
Suggested change
var differ = new Differ();

Copilot uses AI. Check for mistakes.
var diff = InlineDiffBuilder.Diff(before, after, false);

var preview = diff.Lines
.Where(line => line.Type != ChangeType.Unchanged)
.Select(line =>
{
var prefix = line.Type switch
{
ChangeType.Inserted => "+",
ChangeType.Deleted => "-",
ChangeType.Modified => "~",
_ => " "
};
return $"{prefix}{line.Text?.TrimEnd()}";
})
.Where(line => !string.IsNullOrWhiteSpace(line))
.Take(10)
.ToList();

return preview;
}

private static string GetScriptText(DatabaseScriptContainer? script)
{
return script?.Script?.Text ?? string.Empty;
}

private static string? BuildDiffMarkdown(ScriptCompareChange change)
{
var previousScripts = change.From?
.CreateScripts(change.Entity, false)
.GroupBy(script => script.Kind)
.Select(group => group.First())
.ToDictionary(script => script.Kind, script => script)
?? new Dictionary<string, DatabaseScriptContainer>();

var sb = new StringBuilder();
var differ = new Differ();

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This assignment to
differ
is useless, since its value is never read.

Copilot Autofix

AI 5 days ago

To fix the problem, simply remove the unnecessary assignment to the differ variable in the BuildDiffMarkdown method. Specifically, delete line 168 (var differ = new Differ();). No additional imports or logic changes are required, as the Differ instance is not used elsewhere in the method and its removal has no impact on program correctness.


Suggested changeset 1
KustoSchemaTools/Changes/StructuredChangeExtensions.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/KustoSchemaTools/Changes/StructuredChangeExtensions.cs b/KustoSchemaTools/Changes/StructuredChangeExtensions.cs
--- a/KustoSchemaTools/Changes/StructuredChangeExtensions.cs
+++ b/KustoSchemaTools/Changes/StructuredChangeExtensions.cs
@@ -165,7 +165,6 @@
             var previousScripts = BuildPreviousScripts(change.From, change.Entity);
 
             var sb = new StringBuilder();
-            var differ = new Differ();
             foreach (var script in change.Scripts)
             {
                 var before = previousScripts.TryGetValue(script.Kind, out var prior)
EOF
@@ -165,7 +165,6 @@
var previousScripts = BuildPreviousScripts(change.From, change.Entity);

var sb = new StringBuilder();
var differ = new Differ();
foreach (var script in change.Scripts)
{
var before = previousScripts.TryGetValue(script.Kind, out var prior)
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The differ variable is instantiated but never used. The InlineDiffBuilder.Diff method on line 193 doesn't require this variable. Consider removing:

var differ = new Differ();
Suggested change
var differ = new Differ();

Copilot uses AI. Check for mistakes.
foreach (var script in change.Scripts)
{
var before = previousScripts.TryGetValue(script.Kind, out var prior)
? prior.Script?.Text ?? string.Empty
: string.Empty;
var after = script.Script?.Text ?? string.Empty;

if (string.Equals(before, after, StringComparison.Ordinal))
{
continue;
}

var diff = InlineDiffBuilder.Diff(before, after, false);
var hasMeaningfulDiff = diff.Lines.Any(line => line.Type != ChangeType.Unchanged);
if (!hasMeaningfulDiff)
{
continue;
}

if (!string.IsNullOrWhiteSpace(script.Kind))
{
sb.AppendLine($"// {script.Kind}");
}

sb.AppendLine("```diff");
foreach (var line in diff.Lines)
{
var prefix = line.Type switch
{
ChangeType.Inserted => "+",
ChangeType.Deleted => "-",
ChangeType.Modified => "~",
_ => " "
};
sb.AppendLine($"{prefix}{line.Text}");
}
sb.AppendLine("```");
sb.AppendLine();
}

var diffContent = sb.ToString().Trim();
return string.IsNullOrEmpty(diffContent) ? null : diffContent;
}
}
}
Loading
Loading