diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs index 506b043e..5d140b9a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs @@ -60,5 +60,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection /// Returns true is the db connection is to a SQL db /// public bool IsAzure { get; set; } + + /// + /// Returns true if the sql connection is to a DW instance + /// + public bool IsSqlDW { get; set; } + + /// + /// Returns the major version number of the db we are connected to + /// + public int MajorVersion { get; set; } + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index d96eb388..f9092c70 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -19,6 +19,7 @@ using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace; +using Microsoft.SqlServer.Management.Common; namespace Microsoft.SqlTools.ServiceLayer.Connection { @@ -292,6 +293,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection OsVersion = serverInfo.OsVersion }; connectionInfo.IsAzure = serverInfo.IsCloud; + connectionInfo.MajorVersion = serverInfo.ServerMajorVersion; + connectionInfo.IsSqlDW = (serverInfo.EngineEditionId == (int)DatabaseEngineEdition.SqlDataWarehouse); } catch(Exception ex) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index b77e936f..5c0ebfd2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -55,6 +55,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// private readonly List resultSets; + /// + /// Special action which this batch performed + /// + private SpecialAction specialAction; + #endregion internal Batch(string batchText, SelectionData selection, int ordinalId, IFileStreamFactory outputFileFactory) @@ -72,6 +77,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution Id = ordinalId; resultSets = new List(); this.outputFileFactory = outputFileFactory; + specialAction = new SpecialAction(); } #region Events @@ -201,6 +207,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution summary.ResultSetSummaries = ResultSummaries; summary.ExecutionEnd = ExecutionEndTimeStamp; summary.ExecutionElapsed = ExecutionElapsedTime; + summary.SpecialAction = ProcessResultSetSpecialActions(); } return summary; @@ -370,6 +377,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution return targetResultSet.GetSubset(startRow, rowCount); } + /// + /// Generates an execution plan + /// + /// The index for selecting the result set + /// An exeuction plan object + public Task GetExecutionPlan(int resultSetIndex) + { + ResultSet targetResultSet; + lock (resultSets) + { + // Sanity check to make sure we have valid numbers + if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count) + { + throw new ArgumentOutOfRangeException(nameof(resultSetIndex), + SR.QueryServiceSubsetResultSetOutOfRange); + } + + targetResultSet = resultSets[resultSetIndex]; + } + + // Retrieve the result set + return targetResultSet.GetExecutionPlan(); + } + /// /// Saves a result to a file format selected by the user /// @@ -482,6 +513,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// Aggregates all result sets in the batch into a single special action + /// + private SpecialAction ProcessResultSetSpecialActions() + { + foreach (ResultSet resultSet in resultSets) + { + specialAction.CombineSpecialAction(resultSet.Summary.SpecialAction); + } + + return specialAction; + } + #endregion #region IDisposable Implementation diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs index a2c3f0b1..4d21c2b2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs @@ -44,5 +44,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// The summaries of the result sets inside the batch /// public ResultSetSummary[] ResultSetSummaries { get; set; } + + /// + /// The special action of the batch + /// + public SpecialAction SpecialAction { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecutionPlan.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecutionPlan.cs new file mode 100644 index 00000000..b8b0cc42 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecutionPlan.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Class used to represent an execution plan from a query for transmission across JSON RPC + /// + public class ExecutionPlan + { + /// + /// The format of the execution plan + /// + public string Format { get; set; } + + /// + /// The execution plan content + /// + public string Content { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecutionPlanOptions.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecutionPlanOptions.cs new file mode 100644 index 00000000..4054eecc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ExecutionPlanOptions.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Incoming execution plan options from the extension + /// + public struct ExecutionPlanOptions + { + + /// + /// Setting to return the actual execution plan as XML + /// + public bool IncludeActualExecutionPlanXml { get; set; } + + /// + /// Setting to return the estimated execution plan as XML + /// + public bool IncludeEstimatedExecutionPlanXml { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs index b5671fd9..304825f1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs @@ -21,6 +21,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// URI for the editor that is asking for the query execute /// public string OwnerUri { get; set; } + + /// + /// Execution plan options + /// + public ExecutionPlanOptions ExecutionPlanOptions { get; set; } + } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecutionPlanRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecutionPlanRequest.cs new file mode 100644 index 00000000..910e89c0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecutionPlanRequest.cs @@ -0,0 +1,49 @@ +// +// 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.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Parameters for query execution plan request + /// + public class QueryExecutionPlanParams + { + /// + /// URI for the file that owns the query to look up the results for + /// + public string OwnerUri { get; set; } + + /// + /// Index of the batch to get the results from + /// + public int BatchIndex { get; set; } + + /// + /// Index of the result set to get the results from + /// + public int ResultSetIndex { get; set; } + + } + + /// + /// Parameters for the query execution plan request + /// + public class QueryExecutionPlanResult + { + /// + /// The requested execution plan. Optional, can be set to null to indicate an error + /// + public ExecutionPlan ExecutionPlan { get; set; } + } + + public class QueryExecutionPlanRequest + { + public static readonly + RequestType Type = + RequestType.Create("query/executionPlan"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs index e6fc8691..8fd29b02 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs @@ -29,5 +29,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// Details about the columns that are provided as solutions /// public DbColumnWrapper[] ColumnInfo { get; set; } + + /// + /// The special action definition of the result set + /// + public SpecialAction SpecialAction { get; set; } + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 1f23bc0b..48b09b20 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -15,6 +15,7 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Utility; +using System.Collections.Generic; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -51,6 +52,36 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// private bool hasExecuteBeenCalled; + /// + /// Settings for query runtime + /// + private QueryExecutionSettings querySettings; + + /// + /// Streaming output factory for the query + /// + private IFileStreamFactory streamOutputFactory; + + /// + /// ON keyword + /// + private const string On = "ON"; + + /// + /// OFF keyword + /// + private const string Off = "OFF"; + + /// + /// showplan_xml statement + /// + private const string SetShowPlanXml = "SET SHOWPLAN_XML {0}"; + + /// + /// statistics xml statement + /// + private const string SetStatisticsXml = "SET STATISTICS XML {0}"; + #endregion /// @@ -72,6 +103,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution QueryText = queryText; editorConnection = connection; cancellationSource = new CancellationTokenSource(); + querySettings = settings; + streamOutputFactory = outputFactory; // Process the query into batches ParseResult parseResult = Parser.Parse(queryText, new ParseOptions @@ -89,7 +122,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution batch.EndLocation.LineNumber - 1, batch.EndLocation.ColumnNumber - 1), index, outputFactory)); + Batches = batchSelection.ToArray(); + + // Create our batch lists + BeforeBatches = new List(); + AfterBatches = new List(); + + if (DoesSupportExecutionPlan(connection)) + { + // Checking settings for execution plan options + if (querySettings.ExecutionPlanOptions.IncludeEstimatedExecutionPlanXml) + { + // Enable set showplan xml + addBatch(string.Format(SetShowPlanXml, On), BeforeBatches, streamOutputFactory); + addBatch(string.Format(SetShowPlanXml, Off), AfterBatches, streamOutputFactory); + } + else if (querySettings.ExecutionPlanOptions.IncludeActualExecutionPlanXml) + { + addBatch(string.Format(SetStatisticsXml, On), BeforeBatches, streamOutputFactory); + addBatch(string.Format(SetStatisticsXml, Off), AfterBatches, streamOutputFactory); + } + } } #region Events @@ -145,11 +199,21 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The query that completed public delegate Task QueryAsyncEventHandler(Query q); + /// + /// The batches which should run before the user batches + /// + internal List BeforeBatches { get; set; } + /// /// The batches underneath this query /// internal Batch[] Batches { get; set; } + /// + /// The batches which should run after the user batches + /// + internal List AfterBatches { get; set; } + /// /// The summaries of the batches underneath this query /// @@ -241,6 +305,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution return Batches[batchIndex].GetSubset(resultSetIndex, startRow, rowCount); } + /// + /// Retrieves a subset of the result sets + /// + /// The index for selecting the batch item + /// The index for selecting the result set + /// The Execution Plan, if the result set has one + public Task GetExecutionPlan(int batchIndex, int resultSetIndex) + { + // Sanity check to make sure that the batch is within bounds + if (batchIndex < 0 || batchIndex >= Batches.Length) + { + throw new ArgumentOutOfRangeException(nameof(batchIndex), SR.QueryServiceSubsetBatchOutOfRange); + } + + return Batches[batchIndex].GetExecutionPlan(resultSetIndex); + } + /// /// Saves the requested results to a file format of the user's choice /// @@ -316,9 +397,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution try { + // Execute beforeBatches synchronously, before the user defined batches + foreach (Batch b in BeforeBatches) + { + await b.Execute(conn, cancellationSource.Token); + } + // We need these to execute synchronously, otherwise the user will be very unhappy foreach (Batch b in Batches) { + // Add completion callbacks b.BatchStart += BatchStarted; b.BatchCompletion += BatchCompleted; b.BatchMessageSent += BatchMessageSent; @@ -326,6 +414,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution await b.Execute(conn, cancellationSource.Token); } + // Execute afterBatches synchronously, after the user defined batches + foreach (Batch b in AfterBatches) + { + await b.Execute(conn, cancellationSource.Token); + } + // Call the query execution callback if (QueryCompleted != null) { @@ -374,6 +468,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// Function to add a new batch to a Batch set + /// + private void addBatch(string query, List batchSet, IFileStreamFactory outputFactory) + { + batchSet.Add(new Batch(query, null, batchSet.Count, outputFactory)); + } + #endregion #region IDisposable Implementation @@ -403,6 +505,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution disposed = true; } + /// + /// Does this connection support XML Execution plans + /// + private bool DoesSupportExecutionPlan(ConnectionInfo connectionInfo) { + // Determining which execution plan options may be applied (may be added to for pre-yukon support) + return (!connectionInfo.IsSqlDW && connectionInfo.MajorVersion >= 9); + } + #endregion } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index b23e07eb..0b884f7c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -126,6 +126,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution serviceHost.SetRequestHandler(QueryCancelRequest.Type, HandleCancelRequest); serviceHost.SetRequestHandler(SaveResultsAsCsvRequest.Type, HandleSaveResultsAsCsvRequest); serviceHost.SetRequestHandler(SaveResultsAsJsonRequest.Type, HandleSaveResultsAsJsonRequest); + serviceHost.SetRequestHandler(QueryExecutionPlanRequest.Type, HandleExecutionPlanRequest); // Register handler for shutdown event serviceHost.RegisterShutdownTask((shutdownParams, requestContext) => @@ -208,6 +209,36 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// Handles a request to get an execution plan + /// + public async Task HandleExecutionPlanRequest(QueryExecutionPlanParams planParams, + RequestContext requestContext) + { + try + { + // Attempt to load the query + Query query; + if (!ActiveQueries.TryGetValue(planParams.OwnerUri, out query)) + { + await requestContext.SendError(SR.QueryServiceRequestsNoQuery); + return; + } + + // Retrieve the requested execution plan and return it + var result = new QueryExecutionPlanResult + { + ExecutionPlan = await query.GetExecutionPlan(planParams.BatchIndex, planParams.ResultSetIndex) + }; + await requestContext.SendResult(result); + } + catch (Exception e) + { + // This was unexpected, so send back as error + await requestContext.SendError(e.Message); + } + } + /// /// Handles a request to dispose of this query /// @@ -334,6 +365,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Retrieve the current settings for executing the query with QueryExecutionSettings settings = WorkspaceService.CurrentSettings.QueryExecutionSettings; + // Apply execution parameter settings + settings.ExecutionPlanOptions = executeParams.ExecutionPlanOptions; + // Get query text from the workspace. ScriptFile queryFile = WorkspaceService.Workspace.GetFile(executeParams.OwnerUri); @@ -425,6 +459,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution BatchSummary = b.Summary, OwnerUri = executeParams.OwnerUri }; + await requestContext.SendEvent(QueryExecuteBatchStartEvent.Type, eventParams); }; query.BatchStarted += batchStartCallback; @@ -436,6 +471,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution BatchSummary = b.Summary, OwnerUri = executeParams.OwnerUri }; + await requestContext.SendEvent(QueryExecuteBatchCompleteEvent.Type, eventParams); }; query.BatchCompleted += batchCompleteCallback; diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs index 8037178a..cf0e8b67 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs @@ -27,6 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Column names of 'for xml' and 'for json' queries private const string NameOfForXMLColumn = "XML_F52E2B61-18A1-11d1-B105-00805F49916B"; private const string NameOfForJSONColumn = "JSON_F52E2B61-18A1-11d1-B105-00805F49916B"; + private const string YukonXmlShowPlanColumn = "Microsoft SQL Server 2005 XML Showplan"; #endregion @@ -68,6 +69,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// private readonly string outputFileName; + /// + /// The special action which applied to this result set + /// + private SpecialAction specialAction; + #endregion /// @@ -89,6 +95,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Initialize the storage outputFileName = factory.CreateFile(); fileOffsets = new LongList(); + specialAction = new SpecialAction(); // Store the factory fileStreamFactory = factory; @@ -165,7 +172,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution ColumnInfo = Columns, Id = Id, BatchId = BatchId, - RowCount = RowCount + RowCount = RowCount, + SpecialAction = ProcessSpecialAction() + }; } } @@ -236,6 +245,51 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution }); } + /// + /// Generates the execution plan from the table returned + /// + /// An execution plan object + public Task GetExecutionPlan() + { + // Proccess the action just incase is hasn't been yet + ProcessSpecialAction(); + + // Sanity check to make sure that the results have been read beforehand + if (!hasBeenRead) + { + throw new InvalidOperationException(SR.QueryServiceResultSetNotRead); + } + // Check that we this result set contains a showplan + else if (!specialAction.ExpectYukonXMLShowPlan) + { + throw new Exception(SR.QueryServiceExecutionPlanNotFound); + } + + + return Task.Factory.StartNew(() => + { + string content = null; + string format = null; + + using (IFileStreamReader fileStreamReader = fileStreamFactory.GetReader(outputFileName)) + { + // Determine the format and get the first col/row of XML + content = fileStreamReader.ReadRow(0, Columns)[0].DisplayValue; + + if (specialAction.ExpectYukonXMLShowPlan) + { + format = "xml"; + } + } + + return new ExecutionPlan + { + Format = format, + Content = content + }; + }); + } + /// /// Reads from the reader until there are no more results to read /// @@ -436,6 +490,21 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// Determine the special action, if any, for this result set + /// + private SpecialAction ProcessSpecialAction() + { + + // Check if this result set is a showplan + if (dataReader.Columns.Length == 1 && string.Compare(dataReader.Columns[0].ColumnName, YukonXmlShowPlanColumn, StringComparison.OrdinalIgnoreCase) == 0) + { + specialAction.ExpectYukonXMLShowPlan = true; + } + + return specialAction; + } + #endregion } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/SpecialAction.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/SpecialAction.cs new file mode 100644 index 00000000..7d575c1d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/SpecialAction.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +{ + /// + /// Class that represents a Special Action which occured by user request during the query + /// + public class SpecialAction { + + #region Private Class variables + + // Underlying representation as bitwise flags to simplify logic + [Flags] + private enum ActionFlags + { + None = 0, + // All added options must be powers of 2 + ExpectYukonXmlShowPlan = 1 + } + + private ActionFlags flags; + + #endregion + + /// + /// The type of XML execution plan that is contained with in a result set + /// + public SpecialAction() + { + flags = ActionFlags.None; + } + + #region Public Functions + /// + /// No Special action performed + /// + public bool None + { + get { return flags == ActionFlags.None; } + set + { + flags = ActionFlags.None; + } + } + + /// + /// Contains an XML execution plan result set + /// + public bool ExpectYukonXMLShowPlan + { + get { return flags.HasFlag(ActionFlags.ExpectYukonXmlShowPlan); } + set + { + if (value) + { + // OR flags with value to apply + flags |= ActionFlags.ExpectYukonXmlShowPlan; + } + else + { + // AND flags with the inverse of the value we want to remove + flags &= ~(ActionFlags.ExpectYukonXmlShowPlan); + } + } + } + + /// + /// Aggregate this special action with the input + /// + public void CombineSpecialAction(SpecialAction action) + { + flags |= action.flags; + } + + #endregion + }; +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs index a573240c..cd269758 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs @@ -1,6 +1,9 @@ // // 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.ServiceLayer.QueryExecution.Contracts; + namespace Microsoft.SqlTools.ServiceLayer.SqlContext { /// @@ -26,6 +29,16 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext /// private const int DefaultMaxXmlCharsToStore = 2097152; // 2 MB - QE default + /// + /// Default selection of returning an actual XML showplan with all batches + /// Do not return any execution plan by default + /// + private ExecutionPlanOptions DefaultExecutionPlanOptions = new ExecutionPlanOptions() + { + IncludeActualExecutionPlanXml = false, + IncludeEstimatedExecutionPlanXml = false + }; + #endregion #region Member Variables @@ -36,6 +49,8 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext private int? maxXmlCharsToStore; + private ExecutionPlanOptions? executionPlanOptions; + #endregion #region Properties @@ -61,6 +76,12 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext set { maxXmlCharsToStore = value; } } + public ExecutionPlanOptions ExecutionPlanOptions + { + get { return executionPlanOptions ?? DefaultExecutionPlanOptions; } + set { executionPlanOptions = value; } + } + #endregion #region Public Methods @@ -74,6 +95,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext BatchSeparator = newSettings.BatchSeparator; MaxCharsToStore = newSettings.MaxCharsToStore; MaxXmlCharsToStore = newSettings.MaxXmlCharsToStore; + ExecutionPlanOptions = newSettings.ExecutionPlanOptions; } #endregion diff --git a/src/Microsoft.SqlTools.ServiceLayer/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/sr.cs index 1fba1d66..78d207e2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/sr.cs @@ -365,6 +365,14 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string QueryServiceExecutionPlanNotFound + { + get + { + return Keys.GetString(Keys.QueryServiceExecutionPlanNotFound); + } + } + public static string PeekDefinitionNoResultsError { get @@ -623,6 +631,9 @@ namespace Microsoft.SqlTools.ServiceLayer public const string QueryServiceResultSetNoColumnSchema = "QueryServiceResultSetNoColumnSchema"; + public const string QueryServiceExecutionPlanNotFound = "QueryServiceExecutionPlanNotFound"; + + public const string PeekDefinitionAzureError = "PeekDefinitionAzureError"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/sr.resx index 955b2e6e..49f0cd01 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/sr.resx @@ -325,6 +325,10 @@ Could not retrieve column schema for result set + + Could not retrieve an execution plan from the result set + + This feature is currently not supported on Azure SQL DB and Data Warehouse: {0} . diff --git a/src/Microsoft.SqlTools.ServiceLayer/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/sr.strings index ac86e2b6..ff627a91 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/sr.strings @@ -149,6 +149,8 @@ QueryServiceResultSetRowCountOutOfRange = Row count must be a positive integer QueryServiceResultSetNoColumnSchema = Could not retrieve column schema for result set +QueryServiceExecutionPlanNotFound = Could not retrieve an execution plan from the result set + ############################################################################ # Language Service diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index f70c529c..9347857d 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -72,6 +72,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return batch; } + public static Batch GetExecutedBatchWithExecutionPlan() + { + Batch batch = new Batch(StandardQuery, SubsectionDocument, 1, GetFileStreamFactory(new Dictionary())); + batch.Execute(CreateTestConnection(new[] {GetExecutionPlanTestData()}, false), CancellationToken.None).Wait(); + return batch; + } + public static Query GetBasicExecutedQuery() { ConnectionInfo ci = CreateTestConnectionInfo(new[] {StandardTestData}, false); @@ -81,6 +88,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return query; } + public static Query GetBasicExecutedQuery(QueryExecutionSettings querySettings) + { + ConnectionInfo ci = CreateTestConnectionInfo(new[] {StandardTestData}, false); + Query query = new Query(StandardQuery, ci, querySettings, GetFileStreamFactory(new Dictionary())); + query.Execute(); + query.ExecutionTask.Wait(); + return query; + } + public static Dictionary[] GetTestData(int columns, int rows) { Dictionary[] output = new Dictionary[rows]; @@ -97,6 +113,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return output; } + + public static Dictionary[] GetExecutionPlanTestData() + { + Dictionary[] output = new Dictionary[1]; + int col = 0; + int row = 0; + Dictionary rowDictionary = new Dictionary(); + rowDictionary.Add(string.Format("Microsoft SQL Server 2005 XML Showplan", col), string.Format("Execution Plan", col, row)); + output[row] = rowDictionary; + + return output; + } + public static Dictionary[][] GetTestDataSet(int dataSets) { List[]> output = new List[]>(); diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecutionPlanTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecutionPlanTests.cs new file mode 100644 index 00000000..c03becfb --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecutionPlanTests.cs @@ -0,0 +1,242 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class ExecutionPlanTests + { + #region ResultSet Class Tests + + [Fact] + public void ExecutionPlanValid() + { + // Setup: + // ... I have a batch that has been executed with a execution plan + Batch b = Common.GetExecutedBatchWithExecutionPlan(); + + // If: + // ... I have a result set and I ask for a valid execution plan + ResultSet planResultSet = b.ResultSets.First(); + ExecutionPlan plan = planResultSet.GetExecutionPlan().Result; + + // Then: + // ... I should get the execution plan back + Assert.Equal("xml", plan.Format); + Assert.Contains("Execution Plan", plan.Content); + } + + [Fact] + public void ExecutionPlanInvalid() + { + // Setup: + // ... I have a batch that has been executed + Batch b = Common.GetBasicExecutedBatch(); + + // If: + // ... I have a result set and I ask for an execution plan that doesn't exist + ResultSet planResultSet = b.ResultSets.First(); + + // Then: + // ... It should throw an exception + Assert.ThrowsAsync(async () => await planResultSet.GetExecutionPlan()); + } + + #endregion + + #region Batch Class Tests + + [Fact] + public void BatchExecutionPlanValidTest() + { + // If I have an executed batch which has an execution plan + Batch b = Common.GetExecutedBatchWithExecutionPlan(); + + // ... And I ask for a valid execution plan + ExecutionPlan plan = b.GetExecutionPlan(0).Result; + + // Then: + // ... I should get the execution plan back + Assert.Equal("xml", plan.Format); + Assert.Contains("Execution Plan", plan.Content); + } + + [Fact] + public void BatchExecutionPlanInvalidTest() + { + // Setup: + // ... I have a batch that has been executed without an execution plan + Batch b = Common.GetBasicExecutedBatch(); + + // If: + // ... I ask for an invalid execution plan + Assert.ThrowsAsync(async () => await b.GetExecutionPlan(0)); + } + + [Theory] + [InlineData(-1)] // Invalid result set, too low + [InlineData(2)] // Invalid result set, too high + public void BatchExecutionPlanInvalidParamsTest(int resultSetIndex) + { + // If I have an executed batch which has an execution plan + Batch b = Common.GetExecutedBatchWithExecutionPlan(); + + // ... And I ask for an execution plan with an invalid result set index + // Then: + // ... It should throw an exception + Assert.ThrowsAsync(async () => await b.GetExecutionPlan(resultSetIndex)); + } + + #endregion + + #region Query Class Tests + + [Theory] + [InlineData(-1)] // Invalid batch, too low + [InlineData(2)] // Invalid batch, too high + public void QueryExecutionPlanInvalidParamsTest(int batchIndex) + { + // Setup query settings + QueryExecutionSettings querySettings = new QueryExecutionSettings(); + querySettings.ExecutionPlanOptions = new ExecutionPlanOptions() + { + IncludeActualExecutionPlanXml = false, + IncludeEstimatedExecutionPlanXml = true + }; + + // If I have an executed query + Query q = Common.GetBasicExecutedQuery(querySettings); + + // ... And I ask for a subset with an invalid result set index + // Then: + // ... It should throw an exception + Assert.ThrowsAsync(async () => await q.GetExecutionPlan(batchIndex, 0)); + } + + #endregion + + + #region Service Intergration Tests + + [Fact] + public async Task ExecutionPlanServiceValidTest() + { + // If: + // ... I have a query that has results in the form of an execution plan + var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery); + var queryService = Common.GetPrimedExecutionService(new[] {Common.GetExecutionPlanTestData()}, true, false, workspaceService); + var executeParams = new QueryExecuteParams {QuerySelection = null, OwnerUri = Common.OwnerUri}; + executeParams.ExecutionPlanOptions = new ExecutionPlanOptions() + { + IncludeActualExecutionPlanXml = false, + IncludeEstimatedExecutionPlanXml = true + }; + var executeRequest = RequestContextMocks.Create(null); + await queryService.HandleExecuteRequest(executeParams, executeRequest.Object); + await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask; + + // ... And I then ask for a valid execution plan + var executionPlanParams = new QueryExecutionPlanParams { OwnerUri = Common.OwnerUri, BatchIndex = 0, ResultSetIndex = 0 }; + var executionPlanRequest = new EventFlowValidator() + .AddResultValidation(r => + { + // Then: Messages should be null and execution plan should not be null + Assert.NotNull(r.ExecutionPlan); + }).Complete(); + await queryService.HandleExecutionPlanRequest(executionPlanParams, executionPlanRequest.Object); + executionPlanRequest.Validate(); + } + + + [Fact] + public async Task ExecutionPlanServiceMissingQueryTest() + { + // If: + // ... I ask for an execution plan for a file that hasn't executed a query + var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery); + var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); + var executionPlanParams = new QueryExecutionPlanParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + var executionPlanRequest = new EventFlowValidator() + .AddErrorValidation(r => + { + // Then: It should return a populated error + Assert.NotNull(r); + }).Complete(); + await queryService.HandleExecutionPlanRequest(executionPlanParams, executionPlanRequest.Object); + executionPlanRequest.Validate(); + } + + [Fact] + public async Task ExecutionPlanServiceUnexecutedQueryTest() + { + // If: + // ... I have a query that hasn't finished executing (doesn't matter what) + var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery); + var queryService = Common.GetPrimedExecutionService(new[] { Common.GetExecutionPlanTestData() }, true, false, workspaceService); + var executeParams = new QueryExecuteParams { QuerySelection = null, OwnerUri = Common.OwnerUri }; + executeParams.ExecutionPlanOptions = new ExecutionPlanOptions() + { + IncludeActualExecutionPlanXml = false, + IncludeEstimatedExecutionPlanXml = true + }; + var executeRequest = RequestContextMocks.Create(null); + await queryService.HandleExecuteRequest(executeParams, executeRequest.Object); + await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask; + queryService.ActiveQueries[Common.OwnerUri].Batches[0].ResultSets[0].hasBeenRead = false; + + // ... And I then ask for a valid execution plan from it + var executionPlanParams = new QueryExecutionPlanParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + var executionPlanRequest = new EventFlowValidator() + .AddErrorValidation(r => + { + // Then: It should return a populated error + Assert.NotNull(r); + }).Complete(); + await queryService.HandleExecutionPlanRequest(executionPlanParams, executionPlanRequest.Object); + executionPlanRequest.Validate(); + } + + [Fact] + public async Task ExecutionPlanServiceOutOfRangeSubsetTest() + { + // If: + // ... I have a query that doesn't have any result sets + var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery); + var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); + var executeParams = new QueryExecuteParams { QuerySelection = null, OwnerUri = Common.OwnerUri }; + executeParams.ExecutionPlanOptions = new ExecutionPlanOptions() + { + IncludeActualExecutionPlanXml = false, + IncludeEstimatedExecutionPlanXml = true + }; + var executeRequest = RequestContextMocks.Create(null); + await queryService.HandleExecuteRequest(executeParams, executeRequest.Object); + await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask; + + // ... And I then ask for an execution plan from a result set + var executionPlanParams = new QueryExecutionPlanParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + var executionPlanRequest = new EventFlowValidator() + .AddErrorValidation(r => + { + // Then: It should return a populated error + Assert.NotNull(r); + }).Complete(); + await queryService.HandleExecutionPlanRequest(executionPlanParams, executionPlanRequest.Object); + executionPlanRequest.Validate(); + } + + #endregion + + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SpecialActionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SpecialActionTests.cs new file mode 100644 index 00000000..c4882e73 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SpecialActionTests.cs @@ -0,0 +1,79 @@ +// +// 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.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class SpecialActionTests + { + + [Fact] + public void SpecialActionInstantiation() + { + // If: + // ... I create a special action object + var specialAction = new SpecialAction(); + + // Then: + // ... The special action should be set to none and only none + Assert.Equal(true, specialAction.None); + Assert.Equal(false, specialAction.ExpectYukonXMLShowPlan); + } + + [Fact] + public void SpecialActionNoneProperty() + { + // If: + // ... I create a special action object and add properties but set it back to none + var specialAction = new SpecialAction(); + specialAction.ExpectYukonXMLShowPlan = true; + specialAction.None = true; + + // Then: + // ... The special action should be set to none and only none + Assert.Equal(true, specialAction.None); + Assert.Equal(false, specialAction.ExpectYukonXMLShowPlan); + } + + [Fact] + public void SpecialActionExpectYukonXmlShowPlanReset() + { + // If: + // ... I create a special action object and add properties but set the property back to false + var specialAction = new SpecialAction(); + specialAction.ExpectYukonXMLShowPlan = true; + specialAction.ExpectYukonXMLShowPlan = false; + + // Then: + // ... The special action should be set to none and only none + Assert.Equal(true, specialAction.None); + Assert.Equal(false, specialAction.ExpectYukonXMLShowPlan); + } + + [Fact] + public void SpecialActionCombiningProperties() + { + // If: + // ... I create a special action object and add properties and combine with the same property + var specialAction = new SpecialAction(); + specialAction.ExpectYukonXMLShowPlan = true; + + var specialAction2 = new SpecialAction(); + specialAction2.ExpectYukonXMLShowPlan = true; + + specialAction.CombineSpecialAction(specialAction2); + + // Then: + // ... The special action should be set to none and only none + Assert.Equal(false, specialAction.None); + Assert.Equal(true, specialAction.ExpectYukonXMLShowPlan); + } + + + } +}