diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionGraphComparisonResult.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionGraphComparisonResult.cs new file mode 100644 index 00000000..57a54d62 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionGraphComparisonResult.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts +{ + /// + /// An ExecutionGraphComparisonResult is composed of an execution plan node, but has additional properties + /// to keep track of matching ExecutionGraphComparisonResult nodes for execution plan nodes present in the + /// the graph being compared against. This class also features a group index that can assist + /// with coloring similar sections of execution plans in the UI. + /// + public class ExecutionGraphComparisonResult + { + /// + /// The base ExecutionPlanNode for the ExecutionGraphComparisonResult. + /// + public ExecutionPlanNode BaseNode { get; set; } + /// + /// The children of the ExecutionGraphComparisonResult. + /// + public List Children { get; set; } = new List(); + /// + /// The group index of the ExecutionGraphComparisonResult. + /// + public int GroupIndex { get; set; } + /// + /// Flag to indicate if the ExecutionGraphComparisonResult has a matching node in the compared execution plan. + /// + public bool HasMatch { get; set; } + /// + /// List of matching nodes for the ExecutionGraphComparisonResult. + /// + public List MatchingNodes { get; set; } = new List(); + /// + /// The parent of the ExecutionGraphComparisonResult. + /// + public ExecutionGraphComparisonResult ParentNode { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanComparisonRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanComparisonRequest.cs new file mode 100644 index 00000000..80ff1b29 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanComparisonRequest.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts +{ + public class ExecutionPlanComparisonParams + { + /// + /// First query execution plan for comparison. + /// + public ExecutionPlanGraphInfo FirstExecutionPlanGraphInfo { get; set; } + + /// + /// Second query execution plan for comparison. + /// + public ExecutionPlanGraphInfo SecondExecutionPlanGraphInfo { get; set; } + + /// + /// Flag to indicate if the database name should be ignored + /// during comparisons. + /// + public bool IgnoreDatabaseName { get; set; } + } + + public class ExecutionPlanComparisonResult + { + /// + /// Created ExecutionGraphComparisonResult for the first execution plan + /// + public ExecutionGraphComparisonResult FirstComparisonResult { get; set; } + + /// + /// Created ExecutionGraphComparisonResult for the second execution plan + /// + public ExecutionGraphComparisonResult SecondComparisonResult { get; set; } + } + + public class ExecutionPlanComparisonRequest + { + public static readonly + RequestType Type = + RequestType.Create("executionPlan/compareExecutionPlanGraph"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanGraph.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanGraph.cs index e548b91f..ac27249d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanGraph.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/ExecutionPlanGraph.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; -namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan +namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts { /// /// Execution plan graph object that is sent over JSON RPC @@ -32,6 +32,10 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan public class ExecutionPlanNode { + /// + /// ID for the node. + /// + public int ID { get; set; } /// /// Type of the node. This determines the icon that is displayed for it /// @@ -146,7 +150,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan public string QueryWithDescription { get; set; } } - public class ExecutionPlanGraphInfo + public class ExecutionPlanGraphInfo { /// /// File contents diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/GetExecutionPlanRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/GetExecutionPlanRequest.cs index b483936d..c02f283a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/GetExecutionPlanRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/Contracts/GetExecutionPlanRequest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan { diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlanGraphUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanGraphUtils.cs similarity index 69% rename from src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlanGraphUtils.cs rename to src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanGraphUtils.cs index d2b183b8..b4978ad3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlanGraphUtils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanGraphUtils.cs @@ -10,14 +10,15 @@ using System.Linq; using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan; using Microsoft.SqlTools.Utility; using System.Diagnostics; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan { - public class ShowPlanGraphUtils + public class ExecutionPlanGraphUtils { public static List CreateShowPlanGraph(string xml, string fileName) { - ShowPlan.ShowPlanGraph[] graphs = ShowPlan.ShowPlanGraph.ParseShowPlanXML(xml, ShowPlan.ShowPlanType.Unknown); + ShowPlanGraph[] graphs = ShowPlanGraph.ParseShowPlanXML(xml, ShowPlan.ShowPlanType.Unknown); return graphs.Select(g => new ExecutionPlanGraph { Root = ConvertShowPlanTreeToExecutionPlanTree(g.Root), @@ -31,10 +32,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan }).ToList(); } - private static ExecutionPlanNode ConvertShowPlanTreeToExecutionPlanTree(Node currentNode) + public static ExecutionPlanNode ConvertShowPlanTreeToExecutionPlanTree(Node currentNode) { return new ExecutionPlanNode { + ID = currentNode.ID, Type = currentNode.Operation.Image, Cost = currentNode.Cost, SubTreeCost = currentNode.SubtreeCost, @@ -49,7 +51,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan }; } - private static ExecutionPlanEdges ConvertShowPlanEdgeToExecutionPlanEdge(Edge edge) + public static ExecutionPlanEdges ConvertShowPlanEdgeToExecutionPlanEdge(Edge edge) { return new ExecutionPlanEdges { @@ -59,7 +61,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan }; } - private static List GetProperties(PropertyDescriptorCollection props) + public static List GetProperties(PropertyDescriptorCollection props) { List propsList = new List(); foreach (PropertyValue prop in props) @@ -96,7 +98,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan return propsList; } - private static List ParseRecommendations(ShowPlan.ShowPlanGraph g, string fileName) + private static List ParseRecommendations(ShowPlanGraph g, string fileName) { return g.Description.MissingIndices.Select(mi => new ExecutionPlanRecommendation { @@ -148,5 +150,58 @@ GO return String.Empty; } } + + public static void CopyMatchingNodesIntoSkeletonDTO(ExecutionGraphComparisonResult destRoot, ExecutionGraphComparisonResult srcRoot) + { + var srcGraphLookupTable = srcRoot.CreateSkeletonLookupTable(); + + var queue = new Queue(); + queue.Enqueue(destRoot); + + while (queue.Count != 0) + { + var curNode = queue.Dequeue(); + + for (int index = 0; index < curNode.MatchingNodes.Count; ++index) + { + var matchingId = curNode.MatchingNodes[index].BaseNode.ID; + var matchingNode = srcGraphLookupTable[matchingId]; + + curNode.MatchingNodes[index] = matchingNode; + } + + foreach (var child in curNode.Children) + { + queue.Enqueue(child); + } + } + } + } + + public static class ExecutionGraphComparisonResultExtensions + { + public static Dictionary CreateSkeletonLookupTable(this ExecutionGraphComparisonResult node) + { + var skeletonNodeTable = new Dictionary(); + var queue = new Queue(); + queue.Enqueue(node); + + while (queue.Count != 0) + { + var curNode = queue.Dequeue(); + + if (!skeletonNodeTable.ContainsKey(curNode.BaseNode.ID)) + { + skeletonNodeTable[curNode.BaseNode.ID] = curNode; + } + + foreach (var child in curNode.Children) + { + queue.Enqueue(child); + } + } + + return skeletonNodeTable; + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanService.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanService.cs index 59684b87..3f939513 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ExecutionPlanService.cs @@ -6,12 +6,15 @@ using System; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison; using Microsoft.SqlTools.ServiceLayer.Hosting; namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan { /// - /// Main class for Migration Service functionality + /// Main class for Execution Plan Service functionality /// public sealed class ExecutionPlanService : IDisposable { @@ -20,9 +23,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan private bool disposed; /// - /// Construct a new MigrationService instance with default parameters + /// Construct a new Execution Plan Service instance with default parameters /// - public ExecutionPlanService() + private ExecutionPlanService() { } @@ -38,26 +41,23 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan /// Service host object for sending/receiving requests/events. /// Internal for testing purposes. /// - internal IProtocolEndpoint ServiceHost - { - get; - set; - } + internal IProtocolEndpoint ServiceHost { get; set; } /// - /// Initializes the ShowPlan Service instance + /// Initializes the Execution Plan Service instance /// public void InitializeService(ServiceHost serviceHost) { - this.ServiceHost = serviceHost; - this.ServiceHost.SetRequestHandler(GetExecutionPlanRequest.Type, HandleGetExecutionPlan); + ServiceHost = serviceHost; + ServiceHost.SetRequestHandler(GetExecutionPlanRequest.Type, HandleGetExecutionPlan); + ServiceHost.SetRequestHandler(ExecutionPlanComparisonRequest.Type, HandleExecutionPlanComparisonRequest); } private async Task HandleGetExecutionPlan(GetExecutionPlanParams requestParams, RequestContext requestContext) { try { - var plans = ShowPlanGraphUtils.CreateShowPlanGraph(requestParams.GraphInfo.GraphFileContent, ""); + var plans = ExecutionPlanGraphUtils.CreateShowPlanGraph(requestParams.GraphInfo.GraphFileContent, ""); await requestContext.SendResult(new GetExecutionPlanResult { Graphs = plans @@ -70,7 +70,46 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan } /// - /// Disposes the ShowPlan Service + /// Handles requests for color matching similar nodes. + /// + internal async Task HandleExecutionPlanComparisonRequest( + ExecutionPlanComparisonParams requestParams, + RequestContext requestContext) + { + try + { + var firstGraphSet = ShowPlanGraph.ParseShowPlanXML(requestParams.FirstExecutionPlanGraphInfo.GraphFileContent, ShowPlanType.Unknown); + var firstRootNode = firstGraphSet?[0]?.Root; + + var secondGraphSet = ShowPlanGraph.ParseShowPlanXML(requestParams.SecondExecutionPlanGraphInfo.GraphFileContent, ShowPlanType.Unknown); + var secondRootNode = secondGraphSet?[0]?.Root; + + var manager = new SkeletonManager(); + var firstSkeletonNode = manager.CreateSkeleton(firstRootNode); + var secondSkeletonNode = manager.CreateSkeleton(secondRootNode); + manager.ColorMatchingSections(firstSkeletonNode, secondSkeletonNode, requestParams.IgnoreDatabaseName); + + var firstGraphComparisonResultDTO = firstSkeletonNode.ConvertToDTO(); + var secondGraphComparisonResultDTO = secondSkeletonNode.ConvertToDTO(); + ExecutionPlanGraphUtils.CopyMatchingNodesIntoSkeletonDTO(firstGraphComparisonResultDTO, secondGraphComparisonResultDTO); + ExecutionPlanGraphUtils.CopyMatchingNodesIntoSkeletonDTO(secondGraphComparisonResultDTO, firstGraphComparisonResultDTO); + + var result = new ExecutionPlanComparisonResult() + { + FirstComparisonResult = firstGraphComparisonResultDTO, + SecondComparisonResult = secondGraphComparisonResultDTO + }; + + await requestContext.SendResult(result); + } + catch (Exception e) + { + await requestContext.SendError(e.ToString()); + } + } + + /// + /// Disposes the Execution Plan Service /// public void Dispose() { diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonManager.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonManager.cs index 07f91b8b..9216fc95 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonManager.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonManager.cs @@ -22,7 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison /// /// Node to construct skeleton of /// SkeletonNode with children representing logical descendants of the input node - public SkeletonNode CreateSkeleton(Node root) + public SkeletonNode CreateSkeleton(Node root) { Node rootNode = root; var childCount = root.Children.Count; @@ -36,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison } return skeletonParent; } - else if (childCount == 1) + else if (childCount == 1) { if (!ShouldIgnoreDuringComparison(rootNode)) { @@ -63,10 +63,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison public bool AreSkeletonsEquivalent(SkeletonNode root1, SkeletonNode root2, bool ignoreDatabaseName) { if (root1 == null && root2 == null) + { return true; + } if (root1 == null || root2 == null) + { return false; + } if (!root1.BaseNode.IsLogicallyEquivalentTo(root2.BaseNode, ignoreDatabaseName)) { @@ -80,7 +84,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison while (childIterator < root1.Children.Count) { var checkMatch = AreSkeletonsEquivalent(root1.Children.ElementAt(childIterator), root2.Children.ElementAt(childIterator), ignoreDatabaseName); - if (!checkMatch) + if (!checkMatch) { // at least one pair of children (ie inner.Child1 & outer.Child1) didn't match; stop checking rest return false; diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonNode.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonNode.cs index ff3169fd..7d8a7850 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonNode.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Comparison/SkeletonNode.cs @@ -4,19 +4,21 @@ // using System.Collections.Generic; +using System.Linq; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison { public class SkeletonNode { - public Node BaseNode {get; set;} + public Node BaseNode { get; set; } public List MatchingNodes { get; set; } public bool HasMatch { get { return MatchingNodes.Count > 0; } } public SkeletonNode ParentNode { get; set; } public IList Children { get; set; } - public int GroupIndex - { + public int GroupIndex + { get { return this.BaseNode.GroupIndex; @@ -54,7 +56,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison } } - public void AddMatchingSkeletonNode(SkeletonNode match, bool ignoreDatabaseName, bool matchAllChildren=true) + public void AddMatchingSkeletonNode(SkeletonNode match, bool ignoreDatabaseName, bool matchAllChildren = true) { this.BaseNode[NodeBuilderConstants.SkeletonHasMatch] = true; if (matchAllChildren == true) @@ -80,5 +82,43 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison return this.BaseNode.Graph; } + public ExecutionGraphComparisonResult ConvertToDTO() + { + var queue = new Queue(); + queue.Enqueue(this); + + var skeletonNodeDTO = new ExecutionGraphComparisonResult(); + var dtoQueue = new Queue(); + dtoQueue.Enqueue(skeletonNodeDTO); + + while (queue.Count != 0) + { + var curNode = queue.Dequeue(); + var curNodeDTO = dtoQueue.Dequeue(); + + curNodeDTO.BaseNode = ExecutionPlanGraphUtils.ConvertShowPlanTreeToExecutionPlanTree(curNode.BaseNode); + curNodeDTO.GroupIndex = curNode.GroupIndex; + curNodeDTO.HasMatch = curNode.HasMatch; + curNodeDTO.MatchingNodes = curNode.MatchingNodes.Select(matchingNode => + { + var skeletonNodeDTO = new Contracts.ExecutionGraphComparisonResult(); + skeletonNodeDTO.BaseNode = ExecutionPlanGraphUtils.ConvertShowPlanTreeToExecutionPlanTree(matchingNode.BaseNode); + + return skeletonNodeDTO; + }).ToList(); + + foreach (var child in curNode.Children) + { + queue.Enqueue(child); + + var childDTO = new Contracts.ExecutionGraphComparisonResult(); + childDTO.ParentNode = curNodeDTO; + curNodeDTO.Children.Add(childDTO); + dtoQueue.Enqueue(childDTO); + } + } + + return skeletonNodeDTO; + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Graph.cs b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Graph.cs index 6f092f81..8f26d781 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Graph.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ExecutionPlan/ShowPlan/Graph.cs @@ -7,8 +7,8 @@ namespace Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan { public class Graph { - public Node Root; + public Node Root { get; set; } - public Description Description; + public Description Description { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index 9e3286ca..24739657 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -36,6 +36,7 @@ using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.NotebookConvert; using Microsoft.SqlTools.ServiceLayer.ModelManagement; using Microsoft.SqlTools.ServiceLayer.TableDesigner; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan; namespace Microsoft.SqlTools.ServiceLayer { @@ -166,8 +167,8 @@ namespace Microsoft.SqlTools.ServiceLayer InitializeHostedServices(serviceProvider, serviceHost); serviceHost.ServiceProvider = serviceProvider; - ExecutionPlan.ExecutionPlanService.Instance.InitializeService(serviceHost); - serviceProvider.RegisterSingleService(ExecutionPlan.ExecutionPlanService.Instance); + ExecutionPlanService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(ExecutionPlanService.Instance); serviceHost.InitializeRequestHandlers(); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs index fa646582..27bc1de8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecuteRequests/ResultSetEvents.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Microsoft.SqlTools.Hosting.Protocol.Contracts; -using Microsoft.SqlTools.ServiceLayer.ExecutionPlan; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests { diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index aa42c5b0..7a6325fa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -21,6 +21,7 @@ using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.Utility; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -908,7 +909,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution var xmlString = r.GetRow(0)[0].DisplayValue; try { - plans = ShowPlanGraphUtils.CreateShowPlanGraph(xmlString, Path.GetFileName(ownerUri)); + plans = ExecutionPlanGraphUtils.CreateShowPlanGraph(xmlString, Path.GetFileName(ownerUri)); } catch (Exception ex) { diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs index c40399e9..5e539710 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/ShowPlan/ShowPlanTests.cs @@ -9,7 +9,9 @@ using System.IO; using System.Reflection; using NUnit.Framework; using Microsoft.SqlTools.ServiceLayer.ExecutionPlan; - +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.Contracts; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan; +using Microsoft.SqlTools.ServiceLayer.ExecutionPlan.ShowPlan.Comparison; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan { @@ -21,18 +23,265 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan public void ParseXMLFileReturnsValidShowPlanGraph() { ReadFile(".ShowPlan.TestShowPlan.xml"); - var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql"); + var showPlanGraphs = ExecutionPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql"); Assert.AreEqual(1, showPlanGraphs.Count, "exactly one show plan graph should be returned"); Assert.NotNull(showPlanGraphs[0], "graph should not be null"); Assert.NotNull(showPlanGraphs[0].Root, "graph should have a root"); } + [Test] + public void CompareShowPlan_CreateSkeleton() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + Assert.NotNull(rootNode, "graph should have a root"); + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + + Assert.NotNull(skeletonNode, "skeletonNode should not be null"); + } + + [Test] + public void CompareShowPlan_CreateSkeletonUsingDefaultConfiguredNode() + { + var graph = new ShowPlanGraph() + { + Root = null, + Description = new Description() + }; + var context = new NodeBuilderContext(graph, ShowPlanType.Unknown, null); + var node = new Node(default, context); + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(node); + + Assert.NotNull(skeletonNode, "skeletonNode should not be null"); + } + + [Test] + public void CompareShowPlan_DuplicateSkeletons() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + var skeletonNode2 = skeletonManager.CreateSkeleton(rootNode); + + var skeletonCompareResult = skeletonManager.AreSkeletonsEquivalent(skeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsTrue(skeletonCompareResult); + } + + [Test] + public void CompareShowPlan_TwoDefaultConfiguredSkeletons() + { + var graph = new ShowPlanGraph() + { + Root = null, + Description = new Description() + }; + var context = new NodeBuilderContext(graph, ShowPlanType.Unknown, null); + var firstRoot = new Node(default, context); + var secondRoot = new Node(default, context); + + var skeletonManager = new SkeletonManager(); + var firstSkeletonNode = skeletonManager.CreateSkeleton(firstRoot); + var secondSkeletonNode = skeletonManager.CreateSkeleton(secondRoot); + + var skeletonCompareResult = skeletonManager.AreSkeletonsEquivalent(firstSkeletonNode, secondSkeletonNode, ignoreDatabaseName: true); + + Assert.IsTrue(skeletonCompareResult, "The two compared skeleton nodes should be equivalent"); + } + + [Test] + public void CompareShowPlan_ComparingSkeletonAgainstNull() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + SkeletonNode skeletonNode2 = null; + + var skeletonCompareResult = skeletonManager.AreSkeletonsEquivalent(skeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsFalse(skeletonCompareResult); + } + + [Test] + public void CompareShowPlan_DifferentSkeletonChildCount() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + var skeletonNode2 = skeletonManager.CreateSkeleton(rootNode); + skeletonNode2.Children.RemoveAt(skeletonNode2.Children.Count - 1); + + var skeletonCompareResult = skeletonManager.AreSkeletonsEquivalent(skeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsFalse(skeletonCompareResult); + } + + [Test] + public void CompareShowPlan_ColorMatchingSectionsWithEquivalentSkeletons() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + var skeletonNode2 = skeletonManager.CreateSkeleton(rootNode); + + Assert.IsFalse(skeletonNode.HasMatch); + Assert.IsFalse(skeletonNode2.HasMatch); + + Assert.Zero(skeletonNode.MatchingNodes.Count); + Assert.Zero(skeletonNode2.MatchingNodes.Count); + + skeletonManager.ColorMatchingSections(skeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsTrue(skeletonNode.HasMatch); + Assert.IsTrue(skeletonNode2.HasMatch); + + Assert.AreEqual(1, skeletonNode.MatchingNodes.Count); + Assert.AreEqual(1, skeletonNode2.MatchingNodes.Count); + } + + [Test] + public void CompareShowPlan_ColorMatchingSectionsWithNullAndNonNullSkeleton() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + SkeletonNode nullSkeletonNode = null; + var skeletonNode2 = skeletonManager.CreateSkeleton(rootNode); + + skeletonManager.ColorMatchingSections(nullSkeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsNull(nullSkeletonNode); + Assert.IsFalse(skeletonNode2.HasMatch); + Assert.Zero(skeletonNode2.MatchingNodes.Count); + } + + [Test] + public void CompareShowPlan_ColorMatchingSectionsWithTwoNullSKeletons() + { + var skeletonManager = new SkeletonManager(); + SkeletonNode skeletonNode = null; + SkeletonNode skeletonNode2 = null; + + skeletonManager.ColorMatchingSections(skeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsNull(skeletonNode); + Assert.IsNull(skeletonNode2); + } + + [Test] + public void CompareShowPlan_ColorMatchingSectionsWithNonNullAndNullSkeleton() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + SkeletonNode nullSkeletonNode = null; + + skeletonManager.ColorMatchingSections(skeletonNode, nullSkeletonNode, ignoreDatabaseName: true); + + Assert.IsFalse(skeletonNode.HasMatch); + Assert.Zero(skeletonNode.MatchingNodes.Count); + Assert.IsNull(nullSkeletonNode); + } + + [Test] + public void CompareShowPlan_ColorMatchingSectionsWithDifferentChildCount() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var skeletonNode = skeletonManager.CreateSkeleton(rootNode); + var skeletonNode2 = skeletonManager.CreateSkeleton(rootNode); + skeletonNode2.Children.RemoveAt(skeletonNode2.Children.Count - 1); + + Assert.IsFalse(skeletonNode.Children[0].HasMatch); + Assert.IsFalse(skeletonNode2.Children[0].HasMatch); + + Assert.Zero(skeletonNode.Children[0].MatchingNodes.Count); + Assert.Zero(skeletonNode2.Children[0].MatchingNodes.Count); + + skeletonManager.ColorMatchingSections(skeletonNode, skeletonNode2, ignoreDatabaseName: true); + + Assert.IsTrue(skeletonNode.Children[0].HasMatch); + Assert.IsTrue(skeletonNode2.Children[0].HasMatch); + + Assert.AreEqual(1, skeletonNode.Children[0].MatchingNodes.Count); + Assert.AreEqual(1, skeletonNode2.Children[0].MatchingNodes.Count); + } + + [Test] + public void CompareShowPlan_FindNextNonIgnoreNode() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + var skeletonManager = new SkeletonManager(); + var result = skeletonManager.FindNextNonIgnoreNode(rootNode); + + Assert.NotNull(result); + Assert.AreEqual("InnerJoin", result.LogicalOpUnlocName); + } + + [Test] + public void CompareShowPlan_FindNextNonIgnoreNodeWithChildlessRoot() + { + ReadFile(".ShowPlan.TestShowPlan.xml"); + var graphs = ShowPlanGraph.ParseShowPlanXML(this.queryPlanFileText, ShowPlanType.Unknown); + var rootNode = graphs[0].Root; + + for (int childIndex = 0; childIndex < rootNode.Children.Count; ++childIndex) + { + rootNode.Children.RemoveAt(0); + } + + var skeletonManager = new SkeletonManager(); + var result = skeletonManager.FindNextNonIgnoreNode(rootNode); + + Assert.IsNull(result); + } + + [Test] + public void CompareShowPlan_FindNextNonIgnoreNodeWithNullNode() + { + Node rootNode = null; + + var skeletonManager = new SkeletonManager(); + var result = skeletonManager.FindNextNonIgnoreNode(rootNode); + + Assert.IsNull(result); + } + [Test] public void ParsingNestedProperties() { ReadFile(".ShowPlan.TestShowPlan.xml"); string[] commonNestedPropertiesNames = { "MemoryGrantInfo", "OptimizerHardwareDependentProperties" }; - var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql"); + var showPlanGraphs = ExecutionPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql"); ExecutionPlanNode rootNode = showPlanGraphs[0].Root; rootNode.Properties.ForEach(p => { @@ -49,7 +298,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ShowPlan //The first graph in this execution plan has 3 recommendations ReadFile(".ShowPlan.TestShowPlanRecommendations.xml"); string[] commonNestedPropertiesNames = { "MemoryGrantInfo", "OptimizerHardwareDependentProperties" }; - var showPlanGraphs = ShowPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql"); + var showPlanGraphs = ExecutionPlanGraphUtils.CreateShowPlanGraph(queryPlanFileText, "testFile.sql"); List rootNode = showPlanGraphs[0].Recommendations; Assert.AreEqual(3, rootNode.Count, "3 recommendations should be returned by the showplan parser"); }