//
// 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;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
///
/// Single batch of SQL command
///
internal class Batch
{
#region Private methods
///
/// Helper method to format the provided SqlError
///
///
///
private string FormatSqlErrorMessage(SqlError error)
{
string detailedMessage = string.Empty;
if (error.Class > 10)
{
if (string.IsNullOrEmpty(error.Procedure))
{
detailedMessage = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchSqlMessageNoProcedureInfo,
error.Number,
error.Class,
error.State,
error.LineNumber);
}
else
{
detailedMessage = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchSqlMessageWithProcedureInfo,
error.Number,
error.Class,
error.State,
error.Procedure,
error.LineNumber);
}
}
else if (error.Class > 0 && error.Number > 0)
{
detailedMessage = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchSqlMessageNoLineInfo,
error.Number,
error.Class,
error.State);
}
if (!string.IsNullOrEmpty(detailedMessage) && !isSuppressProviderMessageHeaders)
{
detailedMessage = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", error.Source, detailedMessage);
}
return detailedMessage;
}
///
/// Handles a Sql exception
///
///
///
private ScriptExecutionResult HandleSqlException(SqlException ex)
{
ScriptExecutionResult result;
lock (this)
{
if (state == BatchState.Cancelling)
{
result = ScriptExecutionResult.Cancel;
}
else
{
result = ScriptExecutionResult.Failure;
}
}
if (result != ScriptExecutionResult.Cancel)
{
HandleSqlMessages(ex.Errors);
}
return result;
}
///
/// Called when an error message came from SqlClient
///
///
///
///
///
private void RaiseBatchError(string message, SqlError error, TextSpan textSpan)
{
BatchErrorEventArgs args = new BatchErrorEventArgs(message, error, textSpan, null);
RaiseBatchError(args);
}
///
/// Called when an error message came from SqlClient
///
///
private void RaiseBatchError(BatchErrorEventArgs e)
{
EventHandler cache = BatchError;
if (cache != null)
{
cache(this, e);
}
}
///
/// Called when a message came from SqlClient
///
///
/// Additionally, it's being used to notify the user that the script execution
/// has been finished.
///
///
///
private void RaiseBatchMessage(string detailedMessage, string message, SqlError error)
{
EventHandler cache = BatchMessage;
if (cache != null)
{
BatchMessageEventArgs args = new BatchMessageEventArgs(detailedMessage, message, error);
cache(this, args);
}
}
///
/// Called when a new result set has to be processed
///
///
private void RaiseBatchResultSetProcessing(IDataReader dataReader, ShowPlanType expectedShowPlan)
{
EventHandler cache = BatchResultSetProcessing;
if (cache != null)
{
BatchResultSetEventArgs args = new BatchResultSetEventArgs(dataReader, expectedShowPlan);
BatchResultSetProcessing(this, args);
}
}
///
/// Called when the result set has been processed
///
private void RaiseBatchResultSetFinished()
{
EventHandler cache = BatchResultSetFinished;
if (cache != null)
{
cache(this, EventArgs.Empty);
}
}
///
/// Called when the batch is being cancelled with an active result set
///
private void RaiseCancelling()
{
EventHandler cache = BatchCancelling;
if (cache != null)
{
cache(this, EventArgs.Empty);
}
}
#endregion
#region Private enums
private enum BatchState
{
Initial,
Executing,
Executed,
ProcessingResults,
Cancelling,
}
#endregion
#region Private fields
// correspond to public properties
private bool isSuppressProviderMessageHeaders;
private bool isResultExpected = false;
private string sqlText = string.Empty;
private int execTimeout = 30;
private int scriptTrackingId = 0;
private bool isScriptExecutionTracked = false;
private const int ChangeDatabase = 0x1645;
//command that will be used for execution
private IDbCommand command = null;
//current object state
private BatchState state = BatchState.Initial;
//script text to be executed
private TextSpan textSpan;
//index of the batch in collection of batches
private int index = 0;
private long totalAffectedRows = 0;
private bool hasErrors;
// Expected showplan if any
private ShowPlanType expectedShowPlan;
#endregion
#region Constructors
///
/// Default constructor
///
public Batch()
{
// nothing
}
///
/// Creates and initializes a batch object
///
/// Whether it is one of "set [something] on/off" type of command,
/// that doesn't return any results from the server
///
/// Text of the batch
/// Timeout for the batch execution. 0 means no limit
public Batch(string sqlText, bool isResultExpected, int execTimeout)
{
this.isResultExpected = isResultExpected;
this.sqlText = sqlText;
this.execTimeout = execTimeout;
}
#endregion
#region Public properties
///
/// Is the Batch's text valid?
///
public bool HasValidText
{
get
{
return !string.IsNullOrEmpty(sqlText);
}
}
///
/// SQL text that to be executed in the Batch
///
public string Text
{
get
{
return sqlText;
}
set
{
sqlText = value;
}
}
///
/// Determines whether batch execution returns any results
///
public bool IsResultsExpected
{
get
{
return isResultExpected;
}
set
{
isResultExpected = value;
}
}
///
/// Determines the execution timeout for the batch
///
public int ExecutionTimeout
{
get
{
return execTimeout;
}
set
{
execTimeout = value;
}
}
///
/// Determines the textspan to wich the batch belongs to
///
public TextSpan TextSpan
{
get
{
return textSpan;
}
set
{
textSpan = value;
}
}
///
/// Determines the batch index in the collection of batches being executed
///
public int BatchIndex
{
get
{
return index;
}
set
{
index = value;
}
}
///
/// Returns how many rows were affected. It should be the value that can be shown
/// in the UI.
///
///
/// It can be used only after the execution of the batch is finished
///
public long RowsAffected
{
get
{
return totalAffectedRows;
}
}
///
/// Determines if the error.Source should be used when messages are written
///
public bool IsSuppressProviderMessageHeaders
{
get
{
return isSuppressProviderMessageHeaders;
}
set
{
isSuppressProviderMessageHeaders = value;
}
}
///
/// Gets or sets the id of the script we are tracking
///
public int ScriptTrackingId
{
get
{
return scriptTrackingId;
}
set
{
scriptTrackingId = value;
}
}
#endregion
#region Public events
///
/// fired when there is an error message from the server
///
public event EventHandler BatchError = null;
///
/// fired when there is a message from the server
///
public event EventHandler BatchMessage = null;
///
/// fired when there is a new result set available. It is guarnteed
/// to be fired from the same thread that called Execute method
///
public event EventHandler BatchResultSetProcessing = null;
///
/// fired when the batch recieved cancel request BEFORE it
/// initiates cancel operation. Note that it is fired from a
/// different thread then the one used to kick off execution
///
public event EventHandler BatchCancelling = null;
///
/// fired when we've done absolutely all actions for the current result set
///
public event EventHandler BatchResultSetFinished = null;
#endregion
#region Public methods
///
/// Resets the object to its initial state
///
public void Reset()
{
lock (this)
{
state = BatchState.Initial;
command = null;
textSpan = new TextSpan();
totalAffectedRows = 0;
hasErrors = false;
expectedShowPlan = ShowPlanType.None;
isSuppressProviderMessageHeaders = false;
scriptTrackingId = 0;
isScriptExecutionTracked = false;
}
}
///
/// Executes the batch
///
/// Connection to use
/// ShowPlan type to be used
/// result of execution
///
/// It does not return until execution is finished
/// We may have received a Cancel request by the time this function is called
///
public ScriptExecutionResult Execute(SqlConnection connection, ShowPlanType expectedShowPlan)
{
// FUTURE CLEANUP: Remove in favor of general signature (IDbConnection) - #920978
return Execute((IDbConnection)connection, expectedShowPlan);
}
///
/// Executes the batch
///
/// Connection to use
/// ShowPlan type to be used
/// result of execution
///
/// It does not return until execution is finished
/// We may have received a Cancel request by the time this function is called
///
public ScriptExecutionResult Execute(IDbConnection connection, ShowPlanType expectedShowPlan)
{
Validate.IsNotNull(nameof(connection), connection);
//makes sure that the batch is not in use
lock (this)
{
Debug.Assert(command == null, "SQLCommand is NOT null");
if (command != null)
{
command = null;
}
}
this.expectedShowPlan = expectedShowPlan;
return DoBatchExecutionImpl(connection, sqlText);
}
///
/// Cancels the batch
///
///
/// When batch is actually cancelled, Execute() will return with the appropiate status
///
public void Cancel()
{
lock (this)
{
if (state != BatchState.Cancelling)
{
state = BatchState.Cancelling;
RaiseCancelling();
if (command != null)
{
try
{
command.Cancel();
Debug.WriteLine("Batch.Cancel: command.Cancel completed");
}
catch (SqlException)
{
// eat it
}
catch (RetryLimitExceededException)
{
// eat it
}
}
}
}
}
#endregion
#region Protected methods
///
/// Fires an error message event
///
/// Exception caught
///
/// Non-SQL exception
///
protected void HandleExceptionMessage(Exception ex)
{
BatchErrorEventArgs args = new BatchErrorEventArgs(string.Format(CultureInfo.CurrentCulture, SR.EE_BatchError_Exception, ex.Message), ex);
RaiseBatchError(args);
}
///
/// Fires a message event
///
/// SqlClient errors collection
///
/// Sql specific messages.
///
protected void HandleSqlMessages(SqlErrorCollection errors)
{
foreach (SqlError error in errors)
{
if (error.Number == ChangeDatabase)
{
continue;
}
string detailedMessage = FormatSqlErrorMessage(error);
if (error.Class > 10)
{
// expose this event as error
Debug.Assert(detailedMessage.Length != 0);
RaiseBatchError(detailedMessage, error, textSpan);
//at least one error message has been used
hasErrors = true;
}
else
{
RaiseBatchMessage(detailedMessage, error.Message, error);
}
}
}
///
/// method that will be passed as delegate to SqlConnection.InfoMessage
///
protected void OnSqlInfoMessageCallback(object sender, SqlInfoMessageEventArgs e)
{
HandleSqlMessages(e.Errors);
}
///
/// Delegete for SqlCommand.RecordsAffected
///
///
///
///
/// This is exposed as a regular message
///
protected void OnStatementExecutionFinished(object sender, StatementCompletedEventArgs e)
{
string message = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchExecutionInfo_RowsAffected,
e.RecordCount.ToString(System.Globalization.CultureInfo.InvariantCulture));
RaiseBatchMessage(message, message, null);
}
///
/// Called on a new ResultSet on the data reader
///
/// True if result set consumed, false on a Cancel request
///
///
/// The GridStorageResultSet created is owned by the batch consumer. It's only created here.
/// Additionally, when BatchResultSet event handler is called, it won't return until
/// all data is prcessed or the data being processed is terminated (i.e. cancel or error)
///
protected ScriptExecutionResult ProcessResultSet(IDataReader dataReader)
{
if (dataReader == null)
{
throw new ArgumentNullException();
}
Debug.WriteLine("ProcessResultSet: result set has been created");
//initialize result variable that will be set by batch consumer
ScriptExecutionResult scriptExecutionResult = ScriptExecutionResult.Success;
RaiseBatchResultSetProcessing(dataReader, expectedShowPlan);
if (state != BatchState.Cancelling)
{
return scriptExecutionResult;
}
else
{
return ScriptExecutionResult.Cancel;
}
}
// FUTURE CLEANUP: Remove in favor of general signature (IDbConnection) - #920978
protected ScriptExecutionResult DoBatchExecution(SqlConnection connection, string script)
{
return DoBatchExecutionImpl(connection, script);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities"), SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
[SuppressMessage("Microsoft.Usage", "CA2219:DoNotRaiseExceptionsInExceptionClauses")]
private ScriptExecutionResult DoBatchExecutionImpl(IDbConnection connection, string script)
{
Validate.IsNotNull(nameof(connection), connection);
lock (this)
{
if (state == BatchState.Cancelling)
{
state = BatchState.Initial;
return ScriptExecutionResult.Cancel;
}
}
ScriptExecutionResult result = ScriptExecutionResult.Success;
// SqlClient event handlers setup
SqlInfoMessageEventHandler messageHandler = new SqlInfoMessageEventHandler(OnSqlInfoMessageCallback);
StatementCompletedEventHandler statementCompletedHandler = null;
DbConnectionWrapper connectionWrapper = new DbConnectionWrapper(connection);
connectionWrapper.InfoMessage += messageHandler;
IDbCommand command = connection.CreateCommand();
command.CommandText = script;
command.CommandTimeout = execTimeout;
DbCommandWrapper commandWrapper = null;
if (isScriptExecutionTracked && DbCommandWrapper.IsSupportedCommand(command))
{
statementCompletedHandler = new StatementCompletedEventHandler(OnStatementExecutionFinished);
commandWrapper = new DbCommandWrapper(command);
commandWrapper.StatementCompleted += statementCompletedHandler;
}
lock (this)
{
state = BatchState.Executing;
this.command = command;
command = null;
}
try
{
result = this.ExecuteCommand();
}
catch (OutOfMemoryException)
{
throw;
}
catch (SqlException sqlEx)
{
result = HandleSqlException(sqlEx);
}
catch (Exception ex)
{
result = ScriptExecutionResult.Failure;
HandleExceptionMessage(ex);
}
finally
{
if (messageHandler == null)
{
Logger.Write(LogLevel.Error, "Expected handler to be declared");
}
if (null != connectionWrapper)
{
connectionWrapper.InfoMessage -= messageHandler;
}
if (commandWrapper != null)
{
if (statementCompletedHandler == null)
{
Logger.Write(LogLevel.Error, "Expect handler to be declared if we have a command wrapper");
}
commandWrapper.StatementCompleted -= statementCompletedHandler;
}
lock (this)
{
state = BatchState.Initial;
if (command != null)
{
command.Dispose();
command = null;
}
}
}
return result;
}
private ScriptExecutionResult ExecuteCommand()
{
if (command == null)
{
throw new ArgumentNullException("command");
}
return this.ExecuteUnTrackedCommand();
}
private ScriptExecutionResult ExecuteUnTrackedCommand()
{
IDataReader reader = null;
if (!isResultExpected)
{
command.ExecuteNonQuery();
}
else
{
reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
}
return this.CheckStateAndRead(reader);
}
private ScriptExecutionResult CheckStateAndRead(IDataReader reader = null)
{
ScriptExecutionResult result = ScriptExecutionResult.Success;
if (!isResultExpected)
{
lock (this)
{
if (state == BatchState.Cancelling)
{
result = ScriptExecutionResult.Cancel;
}
else
{
result = ScriptExecutionResult.Success;
state = BatchState.Executed;
}
}
}
else
{
lock (this)
{
if (state == BatchState.Cancelling)
{
result = ScriptExecutionResult.Cancel;
}
else
{
state = BatchState.ProcessingResults;
}
}
if (result != ScriptExecutionResult.Cancel)
{
ScriptExecutionResult batchExecutionResult = ScriptExecutionResult.Success;
if (reader != null)
{
bool hasNextResult = false;
do
{
// if there were no results coming from the server, then the FieldCount is 0
if (reader.FieldCount <= 0)
{
hasNextResult = reader.NextResult();
continue;
}
batchExecutionResult = ProcessResultSet(reader);
if (batchExecutionResult != ScriptExecutionResult.Success)
{
result = batchExecutionResult;
break;
}
RaiseBatchResultSetFinished();
hasNextResult = reader.NextResult();
} while (hasNextResult);
}
if (hasErrors)
{
Debug.WriteLine("DoBatchExecution: successfull processed result set, but there were errors shown to the user");
result = ScriptExecutionResult.Failure;
}
if (result != ScriptExecutionResult.Cancel)
{
lock (this)
{
state = BatchState.Executed;
}
}
}
}
if (reader != null)
{
try
{
// reader.Close() doesn't actually close the reader
// so explicitly dispose the reader
reader.Dispose();
reader = null;
}
catch (OutOfMemoryException)
{
throw;
}
catch (SqlException)
{
// nothing
}
}
return result;
}
#endregion
}
}