// // 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.Data.Common; using Microsoft.Data.SqlClient; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.BatchParser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.Utility; 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 { /// /// Internal representation of an active query /// public class Query : IDisposable { #region Constants /// /// "Error" code produced by SQL Server when the database context (name) for a connection changes. /// private const int DatabaseContextChangeErrorNumber = 5701; /// /// 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 #region Member Variables /// /// Cancellation token source, used for cancelling async db actions /// private readonly CancellationTokenSource cancellationSource; /// /// For IDisposable implementation, whether or not this object has been disposed /// private bool disposed; /// /// The connection info associated with the file editor owner URI, used to create a new /// connection upon execution of the query /// private readonly ConnectionInfo editorConnection; /// /// Whether or not the execute method has been called for this query /// private bool hasExecuteBeenCalled; /// /// Name of the new database if the database name was changed in the query /// private string newDatabaseName; #endregion /// /// Constructor for a query /// /// The text of the query to execute /// 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, bool applyExecutionSettings = false) { // Sanity check for input Validate.IsNotNull(nameof(queryText), queryText); Validate.IsNotNull(nameof(connection), connection); Validate.IsNotNull(nameof(settings), settings); Validate.IsNotNull(nameof(outputFactory), outputFactory); // Initialize the internal state QueryText = queryText; editorConnection = connection; cancellationSource = new CancellationTokenSource(); // Process the query into batches BatchParserWrapper parser = new BatchParserWrapper(); ExecutionEngineConditions conditions = null; if (settings.IsSqlCmdMode) { conditions = new ExecutionEngineConditions() { IsSqlCmd = settings.IsSqlCmdMode }; } List parserResult = parser.GetBatches(queryText, conditions); var batchSelection = parserResult .Select((batchDefinition, index) => new Batch(batchDefinition.BatchText, new SelectionData( batchDefinition.StartLine-1, batchDefinition.StartColumn-1, batchDefinition.EndLine-1, batchDefinition.EndColumn-1), index, outputFactory, batchDefinition.BatchExecutionCount, getFullColumnSchema)); Batches = batchSelection.ToArray(); // Create our batch lists BeforeBatches = new List(); AfterBatches = new List(); if (applyExecutionSettings) { ApplyExecutionSettings(connection, settings, outputFactory); } } #region Events /// /// Delegate type for callback when a query completes or fails /// /// The query that completed public delegate Task QueryAsyncEventHandler(Query query); /// /// Delegate type for callback when a query fails /// /// Query that raised the event /// Exception that caused the query to fail public delegate Task QueryAsyncErrorEventHandler(Query query, Exception exception); /// /// Event to be called when a batch is completed. /// public event Batch.BatchAsyncEventHandler BatchCompleted; /// /// Event that will be called when a message has been emitted /// public event Batch.BatchAsyncMessageHandler BatchMessageSent; /// /// Event to be called when a batch starts execution. /// public event Batch.BatchAsyncEventHandler BatchStarted; /// /// Callback for when the query has completed successfully /// public event QueryAsyncEventHandler QueryCompleted; /// /// Callback for when the query has failed /// public event QueryAsyncErrorEventHandler QueryFailed; /// /// Event to be called when a resultset has completed. /// public event ResultSet.ResultSetAsyncEventHandler ResultSetCompleted; /// /// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results. /// public event ResultSet.ResultSetAsyncEventHandler ResultSetAvailable; /// /// Event that will be called when additional rows in the result set are available (rowCount available has increased) /// public event ResultSet.ResultSetAsyncEventHandler ResultSetUpdated; #endregion #region Properties /// /// The batches which should run before the user batches /// private List BeforeBatches { get; } /// /// The batches underneath this query /// internal Batch[] Batches { get; } /// /// The batches which should run after the user batches /// internal List AfterBatches { get; } /// /// The summaries of the batches underneath this query /// public BatchSummary[] BatchSummaries { get { if (!HasExecuted && !HasCancelled && !HasErrored) { throw new InvalidOperationException("Query has not been executed."); } return Batches.Select(b => b.Summary).ToArray(); } } /// /// Storage for the async task for execution. Set as internal in order to await completion /// in unit tests. /// internal Task ExecutionTask { get; private set; } /// /// Whether or not the query has completed executed, regardless of success or failure /// /// /// Don't touch the setter unless you're doing unit tests! /// public bool HasExecuted { get { return Batches.Length == 0 ? hasExecuteBeenCalled : Batches.All(b => b.HasExecuted); } internal set { hasExecuteBeenCalled = value; foreach (var batch in Batches) { batch.HasExecuted = value; } } } /// /// if the query has been cancelled (before execution started) /// public bool HasCancelled { get; private set; } /// /// if the query has errored out (before batch execution started) /// public bool HasErrored { get; private set; } /// /// The text of the query to execute /// public string QueryText { get; } #endregion #region Public Methods /// /// Cancels the query by issuing the cancellation token /// public void Cancel() { // Make sure that the query hasn't completed execution if (HasExecuted) { throw new InvalidOperationException(SR.QueryServiceCancelAlreadyCompleted); } // Issue the cancellation token for the query this.HasCancelled = true; cancellationSource.Cancel(); } /// /// Launches the asynchronous process for executing the query /// public void Execute() { ExecutionTask = Task.Run(ExecuteInternal) .ContinueWithOnFaulted(async t => { if (QueryFailed != null) { await QueryFailed(this, t.Exception); } }); } /// /// Retrieves a subset of the result sets /// /// The index for selecting the batch item /// The index for selecting the result set /// The starting row of the results /// How many rows to retrieve /// A subset of results public Task GetSubset(int batchIndex, int resultSetIndex, long startRow, int rowCount) { Logger.Write(TraceEventType.Start, $"Starting GetSubset execution for batchIndex:'{batchIndex}', resultSetIndex:'{resultSetIndex}', startRow:'{startRow}', rowCount:'{rowCount}'"); // 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].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 /// /// Parameters for the save as request /// /// Factory for creating the reader/writer pair for the requested output format /// /// Delegate to call when the request completes successfully /// Delegate to call if the request fails public void SaveAs(SaveResultsRequestParams saveParams, IFileStreamFactory fileFactory, ResultSet.SaveAsAsyncEventHandler successHandler, ResultSet.SaveAsFailureAsyncEventHandler failureHandler) { // Sanity check to make sure that the batch is within bounds if (saveParams.BatchIndex < 0 || saveParams.BatchIndex >= Batches.Length) { throw new ArgumentOutOfRangeException(nameof(saveParams.BatchIndex), SR.QueryServiceSubsetBatchOutOfRange); } Batches[saveParams.BatchIndex].SaveAs(saveParams, fileFactory, successHandler, failureHandler); } #endregion #region Private Helpers /// /// Executes this query asynchronously and collects all result sets /// private async Task ExecuteInternal() { ReliableSqlConnection sqlConn = null; try { // check for cancellation token before actually making connection cancellationSource.Token.ThrowIfCancellationRequested(); // Mark that we've internally executed hasExecuteBeenCalled = true; // Don't actually execute if there aren't any batches to execute if (Batches.Length == 0) { if (BatchMessageSent != null) { await BatchMessageSent(new ResultMessage(SR.QueryServiceCompletedSuccessfully, false, null)); } if (QueryCompleted != null) { await QueryCompleted(this); } return; } // Locate and setup the connection DbConnection queryConnection = await ConnectionService.Instance.GetOrOpenConnection(editorConnection.OwnerUri, ConnectionType.Query); sqlConn = queryConnection as ReliableSqlConnection; if (sqlConn != null) { // Subscribe to database informational messages sqlConn.GetUnderlyingConnection().FireInfoMessageEventOnUserErrors = true; sqlConn.GetUnderlyingConnection().InfoMessage += OnInfoMessage; } // Execute beforeBatches synchronously, before the user defined batches foreach (Batch b in BeforeBatches) { await b.Execute(queryConnection, 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; b.ResultSetCompletion += ResultSetCompleted; b.ResultSetAvailable += ResultSetAvailable; b.ResultSetUpdated += ResultSetUpdated; await b.Execute(queryConnection, cancellationSource.Token); } // Execute afterBatches synchronously, after the user defined batches foreach (Batch b in AfterBatches) { await b.Execute(queryConnection, cancellationSource.Token); } // Call the query execution callback if (QueryCompleted != null) { await QueryCompleted(this); } } catch (Exception e) { HasErrored = true; if (e is OperationCanceledException) { await BatchMessageSent(new ResultMessage(SR.QueryServiceQueryCancelled, false, null)); } // Call the query failure callback if (QueryFailed != null) { await QueryFailed(this, e); } } finally { // Remove the message handler from the connection if (sqlConn != null) { // Subscribe to database informational messages sqlConn.GetUnderlyingConnection().InfoMessage -= OnInfoMessage; } // If any message notified us we had changed databases, then we must let the connection service know if (newDatabaseName != null) { ConnectionService.Instance.ChangeConnectionDatabaseContext(editorConnection.OwnerUri, newDatabaseName); } foreach (Batch b in Batches) { if (b.HasError) { ConnectionService.EnsureConnectionIsOpen(sqlConn); break; } } } } /// /// Handler for database messages during query execution /// private void OnInfoMessage(object sender, SqlInfoMessageEventArgs args) { SqlConnection conn = sender as SqlConnection; if (conn == null) { throw new InvalidOperationException(SR.QueryServiceMessageSenderNotSql); } foreach (SqlError error in args.Errors) { // Did the database context change (error code 5701)? if (error.Number == DatabaseContextChangeErrorNumber) { newDatabaseName = conn.Database; } } } /// /// Function to add a new batch to a Batch set /// private static void AddBatch(string query, ICollection batchSet, IFileStreamFactory outputFactory) { batchSet.Add(new Batch(query, null, batchSet.Count, outputFactory, 1)); } private void ApplyExecutionSettings( ConnectionInfo connection, QueryExecutionSettings settings, IFileStreamFactory outputFactory) { outputFactory.QueryExecutionSettings = settings; 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 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) { return; } if (disposing) { cancellationSource.Dispose(); foreach (Batch b in Batches) { b.Dispose(); } } 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 } }