diff --git a/src/Microsoft.SqlTools.Hosting/Utility/GeneralRequestDetails.cs b/src/Microsoft.SqlTools.Hosting/Utility/GeneralRequestDetails.cs index b88cf3d0..707d8206 100644 --- a/src/Microsoft.SqlTools.Hosting/Utility/GeneralRequestDetails.cs +++ b/src/Microsoft.SqlTools.Hosting/Utility/GeneralRequestDetails.cs @@ -17,9 +17,9 @@ namespace Microsoft.SqlTools.Utility Options = new Dictionary(); } - public T GetOptionValue(string name) + public T GetOptionValue(string name, T defaultValue = default(T)) { - T result = default(T); + T result = defaultValue; if (Options != null && Options.ContainsKey(name)) { object value = Options[name]; @@ -29,7 +29,7 @@ namespace Microsoft.SqlTools.Utility } catch { - result = default(T); + result = defaultValue; Logger.Write(TraceEventType.Warning, string.Format(CultureInfo.InvariantCulture, "Cannot convert option value {0}:{1} to {2}", name, value ?? "", typeof(T))); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobNotificationsActions.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobNotificationsActions.cs index ba0582b5..c138aeb2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobNotificationsActions.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobNotificationsActions.cs @@ -21,7 +21,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent internal class JobNotificationsActions : ManagementActionBase { private JobData data; - private bool loading = false; public JobNotificationsActions(CDataContainer dataContainer, JobData data) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index 603037bf..18fb839c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -545,10 +545,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// /// Handle the file open notification /// + /// /// /// /// public async Task HandleDidOpenTextDocumentNotification( + string uri, ScriptFile scriptFile, EventContext eventContext) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs index f8190a45..08a54d72 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Management/Common/ManagementActionBase.cs @@ -44,13 +44,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management private CDataContainer dataContainer; //whether we assume complete ownership over it. //We set this member once the dataContainer is set to be non-null - private bool ownDataContainer = true; - - //if derived class tries to call a protected method that relies on service provider, - //and the service provider hasn't been set yet, we will cache the values and will - //propagate them when we get the provider set - //private System.Drawing.Icon cachedIcon = null; - private string cachedCaption = null; + private bool ownDataContainer = true; //SMO Server connection that MUST be used for all enumerator calls //We'll get this object out of CDataContainer, that must be initialized diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index a7c470f2..7a4cfa10 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -20,6 +20,7 @@ using Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode; using System.Collections.Generic; using System.Diagnostics; using Microsoft.SqlTools.ServiceLayer.Utility; +using System.Text; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -94,7 +95,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The information of the connection to use to execute the query /// Settings for how to execute the query, from the user /// Factory for creating output files - public Query(string queryText, ConnectionInfo connection, QueryExecutionSettings settings, IFileStreamFactory outputFactory, bool getFullColumnSchema = false) + public Query( + string queryText, + ConnectionInfo connection, + QueryExecutionSettings settings, + IFileStreamFactory outputFactory, + bool getFullColumnSchema = false, + bool applyExecutionSettings = false) { // Sanity check for input Validate.IsNotNull(nameof(queryText), queryText); @@ -129,20 +136,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution BeforeBatches = new List(); AfterBatches = new List(); - if (DoesSupportExecutionPlan(connection)) + if (applyExecutionSettings) { - // Checking settings for execution plan options - if (settings.ExecutionPlanOptions.IncludeEstimatedExecutionPlanXml) - { - // Enable set showplan xml - AddBatch(string.Format(SetShowPlanXml, On), BeforeBatches, outputFactory); - AddBatch(string.Format(SetShowPlanXml, Off), AfterBatches, outputFactory); - } - else if (settings.ExecutionPlanOptions.IncludeActualExecutionPlanXml) - { - AddBatch(string.Format(SetStatisticsXml, On), BeforeBatches, outputFactory); - AddBatch(string.Format(SetStatisticsXml, Off), AfterBatches, outputFactory); - } + ApplyExecutionSettings(connection, settings, outputFactory); } } @@ -509,6 +505,118 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution batchSet.Add(new Batch(query, null, batchSet.Count, outputFactory, 1)); } + private void ApplyExecutionSettings( + ConnectionInfo connection, + QueryExecutionSettings settings, + IFileStreamFactory outputFactory) + { + QuerySettingsHelper helper = new QuerySettingsHelper(settings); + + // set query execution plan options + if (DoesSupportExecutionPlan(connection)) + { + // Checking settings for execution plan options + if (settings.ExecutionPlanOptions.IncludeEstimatedExecutionPlanXml) + { + // Enable set showplan xml + AddBatch(string.Format(SetShowPlanXml, On), BeforeBatches, outputFactory); + AddBatch(string.Format(SetShowPlanXml, Off), AfterBatches, outputFactory); + } + else if (settings.ExecutionPlanOptions.IncludeActualExecutionPlanXml) + { + AddBatch(string.Format(SetStatisticsXml, On), BeforeBatches, outputFactory); + AddBatch(string.Format(SetStatisticsXml, Off), AfterBatches, outputFactory); + } + } + + StringBuilder builderBefore = new StringBuilder(512); + StringBuilder builderAfter = new StringBuilder(512); + + if (!connection.IsSqlDW) + { + // "set noexec off" should be the very first command, cause everything after + // corresponding "set noexec on" is not executed until "set noexec off" + // is encounted + if (!settings.NoExec) + { + builderBefore.AppendFormat("{0} ", helper.SetNoExecString); + } + + if (settings.StatisticsIO) + { + builderBefore.AppendFormat("{0} ", helper.GetSetStatisticsIOString(true)); + builderAfter.AppendFormat("{0} ", helper.GetSetStatisticsIOString (false)); + } + + if (settings.StatisticsTime) + { + builderBefore.AppendFormat("{0} ", helper.GetSetStatisticsTimeString (true)); + builderAfter.AppendFormat("{0} ", helper.GetSetStatisticsTimeString(false)); + } + } + + if (settings.ParseOnly) + { + builderBefore.AppendFormat("{0} ", helper.GetSetParseOnlyString(true)); + builderAfter.AppendFormat("{0} ", helper.GetSetParseOnlyString(false)); + } + + // append first part of exec options + builderBefore.AppendFormat("{0} {1} {2}", + helper.SetRowCountString, helper.SetTextSizeString, helper.SetNoCountString); + + if (!connection.IsSqlDW) + { + // append second part of exec options + builderBefore.AppendFormat(" {0} {1} {2} {3} {4} {5} {6}", + helper.SetConcatenationNullString, + helper.SetArithAbortString, + helper.SetLockTimeoutString, + helper.SetQueryGovernorCostString, + helper.SetDeadlockPriorityString, + helper.SetTransactionIsolationLevelString, + // We treat XACT_ABORT special in that we don't add anything if the option + // isn't checked. This is because we don't want to be overwriting the server + // if it has a default of ON since that's something people would specifically + // set and having a client change it could be dangerous (the reverse is much + // less risky) + + // The full fix would probably be to make the options tri-state instead of + // just on/off, where the default is to use the servers default. Until that + // happens though this is the best solution we came up with. See TFS#7937925 + + // Note that users can always specifically add SET XACT_ABORT OFF to their + // queries if they do truly want to set it off. We just don't want to + // do it silently (since the default is going to be off) + settings.XactAbortOn ? helper.SetXactAbortString : string.Empty); + + // append Ansi options + builderBefore.AppendFormat(" {0} {1} {2} {3} {4} {5} {6}", + helper.SetAnsiNullsString, helper.SetAnsiNullDefaultString, helper.SetAnsiPaddingString, + helper.SetAnsiWarningsString, helper.SetCursorCloseOnCommitString, + helper.SetImplicitTransactionString, helper.SetQuotedIdentifierString); + + // "set noexec on" should be the very last command, cause everything after it is not + // being executed unitl "set noexec off" is encounered + if (settings.NoExec) + { + builderBefore.AppendFormat("{0} ", helper.SetNoExecString); + } + } + + // add connection option statements before query execution + if (builderBefore.Length > 0) + { + AddBatch(builderBefore.ToString(), BeforeBatches, outputFactory); + } + + // add connection option statements after query execution + if (builderAfter.Length > 0) + { + AddBatch(builderAfter.ToString(), AfterBatches, outputFactory); + } + } + #endregion #region IDisposable Implementation @@ -541,7 +649,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// Does this connection support XML Execution plans /// - private bool DoesSupportExecutionPlan(ConnectionInfo connectionInfo) { + 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); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionOptionsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionOptionsRequest.cs new file mode 100644 index 00000000..4dcadae0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionOptionsRequest.cs @@ -0,0 +1,27 @@ +// +// 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; +using Microsoft.SqlTools.ServiceLayer.SqlContext; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Parameters for the query execution options request + /// + public class QueryExecutionOptionsParams + { + public string OwnerUri { get; set; } + + public QueryExecutionSettings Options { get; set; } + } + + public class QueryExecutionOptionsRequest + { + public static readonly + RequestType Type = + RequestType.Create("query/setexecutionoptions"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 2eece988..5db2ee89 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -109,6 +109,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// internal ConcurrentDictionary ActiveQueries => queries.Value; + /// + /// The collection of query execution options + /// + internal ConcurrentDictionary ActiveQueryExecutionSettings => queryExecutionSettings.Value; + /// /// Internal task for testability /// @@ -127,6 +132,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution private readonly Lazy> queries = new Lazy>(() => new ConcurrentDictionary()); + /// + /// Internal storage of active query settings + /// + private readonly Lazy> queryExecutionSettings = + new Lazy>(() => new ConcurrentDictionary()); + /// /// Settings that will be used to execute queries. Internal for unit testing /// @@ -165,6 +176,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution serviceHost.SetRequestHandler(SaveResultsAsXmlRequest.Type, HandleSaveResultsAsXmlRequest); serviceHost.SetRequestHandler(QueryExecutionPlanRequest.Type, HandleExecutionPlanRequest); serviceHost.SetRequestHandler(SimpleExecuteRequest.Type, HandleSimpleExecuteRequest); + serviceHost.SetRequestHandler(QueryExecutionOptionsRequest.Type, HandleQueryExecutionOptionsRequest); + + // Register the file open update handler + WorkspaceService.Instance.RegisterTextDocCloseCallback(HandleDidCloseTextDocumentNotification); // Register handler for shutdown event serviceHost.RegisterShutdownTask((shutdownParams, requestContext) => @@ -203,7 +218,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Use the internal handler to launch the query WorkTask = Task.Run(async () => { - await InterServiceExecuteQuery(executeParams, null, requestContext, queryCreateSuccessAction, queryCreateFailureAction, null, null); + await InterServiceExecuteQuery( + executeParams, + null, + requestContext, + queryCreateSuccessAction, + queryCreateFailureAction, + null, + null, + isQueryEditor(executeParams.OwnerUri)); }); } catch (Exception ex) @@ -351,6 +374,33 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + + /// + /// Handles a request to set query execution options + /// + internal async Task HandleQueryExecutionOptionsRequest(QueryExecutionOptionsParams queryExecutionOptionsParams, + RequestContext requestContext) + { + try + { + string uri = queryExecutionOptionsParams.OwnerUri; + if (ActiveQueryExecutionSettings.ContainsKey(uri)) + { + QueryExecutionSettings settings; + ActiveQueryExecutionSettings.TryRemove(uri, out settings); + } + + ActiveQueryExecutionSettings.TryAdd(uri, queryExecutionOptionsParams.Options); + + await requestContext.SendResult(true); + } + catch (Exception e) + { + // This was unexpected, so send back as error + await requestContext.SendError(e.Message); + } + } + /// /// Handles a request to get an execution plan /// @@ -524,7 +574,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution Func> queryCreateSuccessFunc, Func queryCreateFailFunc, Query.QueryAsyncEventHandler querySuccessFunc, - Query.QueryAsyncErrorEventHandler queryFailureFunc) + Query.QueryAsyncErrorEventHandler queryFailureFunc, + bool applyExecutionSettings = false) { Validate.IsNotNull(nameof(executeParams), executeParams); Validate.IsNotNull(nameof(queryEventSender), queryEventSender); @@ -533,7 +584,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution try { // Get a new active query - newQuery = CreateQuery(executeParams, connInfo); + newQuery = CreateQuery(executeParams, connInfo, applyExecutionSettings); if (queryCreateSuccessFunc != null && !await queryCreateSuccessFunc(newQuery)) { // The callback doesn't want us to continue, for some reason @@ -615,19 +666,49 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution subsetParams.RowsStartIndex, subsetParams.RowsCount); } + /// + /// Handle the file open notification + /// + /// + /// + /// + public async Task HandleDidCloseTextDocumentNotification( + string uri, + ScriptFile scriptFile, + EventContext eventContext) + { + try + { + // remove any query execution settings when an editor is closed + if (this.ActiveQueryExecutionSettings.ContainsKey(uri)) + { + QueryExecutionSettings settings; + this.ActiveQueryExecutionSettings.TryRemove(uri, out settings); + } + } + catch (Exception ex) + { + Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString()); + } + await Task.FromResult(true); + } + #endregion #region Private Helpers - - - private Query CreateQuery(ExecuteRequestParamsBase executeParams, ConnectionInfo connInfo) + private Query CreateQuery( + ExecuteRequestParamsBase executeParams, + ConnectionInfo connInfo, + bool applyExecutionSettings) { // Attempt to get the connection for the editor ConnectionInfo connectionInfo; - if (connInfo != null) { + if (connInfo != null) + { connectionInfo = connInfo; - } else if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + } + else if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) { throw new ArgumentOutOfRangeException(nameof(executeParams.OwnerUri), SR.QueryServiceQueryInvalidOwnerUri); } @@ -643,15 +724,39 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution oldQuery.Dispose(); ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery); } - - // Retrieve the current settings for executing the query with - QueryExecutionSettings settings = Settings.QueryExecutionSettings; - - // Apply execution parameter settings - settings.ExecutionPlanOptions = executeParams.ExecutionPlanOptions; + + // check if there are active query execution settings for the editor, otherwise, use the global settings + QueryExecutionSettings settings; + if (this.ActiveQueryExecutionSettings.TryGetValue(executeParams.OwnerUri, out settings)) + { + // special-case handling for query plan options to maintain compat with query execution API parameters + // the logic is that if either the query execute API parameters or the active query setttings + // request a plan then enable the query option + ExecutionPlanOptions executionPlanOptions = executeParams.ExecutionPlanOptions; + if (settings.IncludeActualExecutionPlanXml) + { + executionPlanOptions.IncludeActualExecutionPlanXml = settings.IncludeActualExecutionPlanXml; + } + if (settings.IncludeEstimatedExecutionPlanXml) + { + executionPlanOptions.IncludeEstimatedExecutionPlanXml = settings.IncludeEstimatedExecutionPlanXml; + } + settings.ExecutionPlanOptions = executionPlanOptions; + } + else + { + settings = Settings.QueryExecutionSettings; + settings.ExecutionPlanOptions = executeParams.ExecutionPlanOptions; + } // If we can't add the query now, it's assumed the query is in progress - Query newQuery = new Query(GetSqlText(executeParams), connectionInfo, settings, BufferFileFactory, executeParams.GetFullColumnSchema); + Query newQuery = new Query( + GetSqlText(executeParams), + connectionInfo, + settings, + BufferFileFactory, + executeParams.GetFullColumnSchema, + applyExecutionSettings); if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) { newQuery.Dispose(); @@ -949,6 +1054,18 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution disposed = true; } + /// + /// Verify if the URI maps to a query editor document + /// + /// + /// + private bool isQueryEditor(string uri) + { + return (!string.IsNullOrWhiteSpace(uri) + && (uri.StartsWith("untitled:") + || uri.StartsWith("file:"))); + } + ~QueryExecutionService() { Dispose(false); diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QuerySettingsHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QuerySettingsHelper.cs new file mode 100644 index 00000000..35d50cc4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QuerySettingsHelper.cs @@ -0,0 +1,223 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.SqlContext; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +{ + /// + /// Service for executing queries + /// + public class QuerySettingsHelper + { + //strings for various "SET