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);
+ }
+
+
+ }
+}