//
// 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.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
///
/// This class represents a batch within a query
///
public class Batch : IDisposable
{
#region Member Variables
///
/// For IDisposable implementation, whether or not this has been disposed
///
private bool disposed;
///
/// Local time when the execution and retrieval of files is finished
///
private DateTime executionEndTime;
///
/// Local time when the execution starts, specifically when the object is created
///
private DateTime executionStartTime;
///
/// Factory for creating readers/writers for the output of the batch
///
private readonly IFileStreamFactory outputFileFactory;
///
/// Internal representation of the messages so we can modify internally
///
private readonly List resultMessages;
///
/// Internal representation of the result sets so we can modify internally
///
private readonly List resultSets;
#endregion
internal Batch(string batchText, SelectionData selection, int ordinalId, IFileStreamFactory outputFileFactory)
{
// Sanity check for input
Validate.IsNotNullOrEmptyString(nameof(batchText), batchText);
Validate.IsNotNull(nameof(outputFileFactory), outputFileFactory);
Validate.IsGreaterThan(nameof(ordinalId), ordinalId, 0);
// Initialize the internal state
BatchText = batchText;
Selection = selection;
executionStartTime = DateTime.Now;
HasExecuted = false;
Id = ordinalId;
resultSets = new List();
resultMessages = new List();
this.outputFileFactory = outputFileFactory;
}
#region Properties
///
/// Asynchronous handler for when batches are completed
///
/// The batch that completed
public delegate Task BatchAsyncEventHandler(Batch batch);
///
/// Event that will be called when the batch has completed execution
///
public event BatchAsyncEventHandler BatchCompletion;
///
/// The text of batch that will be executed
///
public string BatchText { get; set; }
///
/// Localized timestamp for when the execution completed.
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
///
public string ExecutionEndTimeStamp { get { return executionEndTime.ToString("o"); } }
///
/// Localized timestamp for how long it took for the execution to complete
///
public string ExecutionElapsedTime
{
get
{
TimeSpan elapsedTime = executionEndTime - executionStartTime;
return elapsedTime.ToString();
}
}
///
/// Localized timestamp for when the execution began.
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
///
public string ExecutionStartTimeStamp { get { return executionStartTime.ToString("o"); } }
///
/// Whether or not this batch has an error
///
public bool HasError { get; set; }
///
/// Whether or not this batch has been executed, regardless of success or failure
///
public bool HasExecuted { get; set; }
///
/// Ordinal of the batch in the query
///
public int Id { get; private set; }
///
/// Messages that have come back from the server
///
public IEnumerable ResultMessages
{
get { return resultMessages; }
}
///
/// The result sets of the batch execution
///
public IList ResultSets
{
get { return resultSets; }
}
///
/// Property for generating a set result set summaries from the result sets
///
public ResultSetSummary[] ResultSummaries
{
get
{
return ResultSets.Select((set, index) => new ResultSetSummary()
{
ColumnInfo = set.Columns,
Id = index,
RowCount = set.RowCount
}).ToArray();
}
}
///
/// Creates a based on the batch instance
///
public BatchSummary Summary
{
get
{
return new BatchSummary
{
HasError = HasError,
Id = Id,
ResultSetSummaries = ResultSummaries,
Messages = ResultMessages.ToArray(),
Selection = Selection,
ExecutionElapsed = ExecutionElapsedTime,
ExecutionStart = ExecutionStartTimeStamp,
ExecutionEnd = ExecutionEndTimeStamp
};
}
}
///
/// The range from the file that is this batch
///
internal SelectionData Selection { get; set; }
#endregion
#region Public Methods
///
/// Executes this batch and captures any server messages that are returned.
///
/// The connection to use to execute the batch
/// Token for cancelling the execution
public async Task Execute(DbConnection conn, CancellationToken cancellationToken)
{
// Sanity check to make sure we haven't already run this batch
if (HasExecuted)
{
throw new InvalidOperationException("Batch has already executed.");
}
try
{
// Register the message listener to *this instance* of the batch
// Note: This is being done to associate messages with batches
ReliableSqlConnection sqlConn = conn as ReliableSqlConnection;
DbCommand command;
if (sqlConn != null)
{
// Register the message listener to *this instance* of the batch
// Note: This is being done to associate messages with batches
sqlConn.GetUnderlyingConnection().InfoMessage += StoreDbMessage;
command = sqlConn.GetUnderlyingConnection().CreateCommand();
// Add a handler for when the command completes
SqlCommand sqlCommand = (SqlCommand) command;
sqlCommand.StatementCompleted += StatementCompletedHandler;
}
else
{
command = conn.CreateCommand();
}
// Make sure we aren't using a ReliableCommad since we do not want automatic retry
Debug.Assert(!(command is ReliableSqlConnection.ReliableSqlCommand),
"ReliableSqlCommand command should not be used to execute queries");
// Create a command that we'll use for executing the query
using (command)
{
command.CommandText = BatchText;
command.CommandType = CommandType.Text;
command.CommandTimeout = 0;
executionStartTime = DateTime.Now;
// Execute the command to get back a reader
using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken))
{
do
{
// Skip this result set if there aren't any rows (ie, UPDATE/DELETE/etc queries)
if (!reader.HasRows && reader.FieldCount == 0)
{
continue;
}
// This resultset has results (ie, SELECT/etc queries)
ResultSet resultSet = new ResultSet(reader, outputFileFactory);
// Add the result set to the results of the query
resultSets.Add(resultSet);
// Read until we hit the end of the result set
await resultSet.ReadResultToEnd(cancellationToken).ConfigureAwait(false);
} while (await reader.NextResultAsync(cancellationToken));
// If there were no messages, for whatever reason (NO COUNT set, messages
// were emitted, records returned), output a "successful" message
if (resultMessages.Count == 0)
{
resultMessages.Add(new ResultMessage(SR.QueryServiceCompletedSuccessfully));
}
}
}
}
catch (DbException dbe)
{
HasError = true;
UnwrapDbException(dbe);
}
catch (TaskCanceledException)
{
resultMessages.Add(new ResultMessage(SR.QueryServiceQueryCancelled));
throw;
}
catch (Exception e)
{
HasError = true;
resultMessages.Add(new ResultMessage(SR.QueryServiceQueryFailed(e.Message)));
throw;
}
finally
{
// Remove the message event handler from the connection
ReliableSqlConnection sqlConn = conn as ReliableSqlConnection;
if (sqlConn != null)
{
sqlConn.GetUnderlyingConnection().InfoMessage -= StoreDbMessage;
}
// Mark that we have executed
HasExecuted = true;
executionEndTime = DateTime.Now;
// Fire an event to signify that the batch has completed
if (BatchCompletion != null)
{
await BatchCompletion(this);
}
}
}
///
/// Generates a subset of the rows from a result set of the batch
///
/// 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 resultSetIndex, int startRow, int rowCount)
{
// Sanity check to make sure that the batch has finished
if (!HasExecuted)
{
throw new InvalidOperationException(SR.QueryServiceSubsetBatchNotCompleted);
}
// Sanity check to make sure we have valid numbers
if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count)
{
throw new ArgumentOutOfRangeException(nameof(resultSetIndex), SR.QueryServiceSubsetResultSetOutOfRange);
}
// Retrieve the result set
return resultSets[resultSetIndex].GetSubset(startRow, rowCount);
}
#endregion
#region Private Helpers
///
/// Handler for when the StatementCompleted event is fired for this batch's command. This
/// will be executed ONLY when there is a rowcount to report. If this event is not fired
/// either NOCOUNT has been set or the command doesn't affect records.
///
/// Sender of the event
/// Arguments for the event
private void StatementCompletedHandler(object sender, StatementCompletedEventArgs args)
{
// Add a message for the number of rows the query returned
string message;
if (args.RecordCount == 1)
{
message = SR.QueryServiceAffectedOneRow;
}
else
{
message = SR.QueryServiceAffectedRows(args.RecordCount);
}
resultMessages.Add(new ResultMessage(message));
}
///
/// Delegate handler for storing messages that are returned from the server
/// NOTE: Only messages that are below a certain severity will be returned via this
/// mechanism. Anything above that level will trigger an exception.
///
/// Object that fired the event
/// Arguments from the event
private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args)
{
resultMessages.Add(new ResultMessage(args.Message));
}
///
/// Attempts to convert a to a that
/// contains much more info about Sql Server errors. The exception is then unwrapped and
/// messages are formatted and stored in . If the exception
/// cannot be converted to SqlException, the message is written to the messages list.
///
/// The exception to unwrap
private void UnwrapDbException(DbException dbe)
{
SqlException se = dbe as SqlException;
if (se != null)
{
var errors = se.Errors.Cast().ToList();
// Detect user cancellation errors
if (errors.Any(error => error.Class == 11 && error.Number == 0))
{
// User cancellation error, add the single message
HasError = false;
resultMessages.Add(new ResultMessage(SR.QueryServiceQueryCancelled));
}
else
{
// Not a user cancellation error, add all
foreach (var error in errors)
{
int lineNumber = error.LineNumber + Selection.StartLine;
string message = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
error.Number, error.Class, error.State, lineNumber,
Environment.NewLine, error.Message);
resultMessages.Add(new ResultMessage(message));
}
}
}
else
{
resultMessages.Add(new ResultMessage(dbe.Message));
}
}
#endregion
#region IDisposable Implementation
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
foreach (ResultSet r in ResultSets)
{
r.Dispose();
}
}
disposed = true;
}
#endregion
}
}