Files
sqltoolsservice/src/Microsoft.Kusto.ServiceLayer/QueryExecution/Batch.cs
Charles Gagnon 9034b397ac Error on CS8600 (#2112)
* Error on CS8600

* couple more
2023-06-26 08:52:51 -07:00

650 lines
24 KiB
C#

//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kusto.ServiceLayer.Connection;
using Microsoft.Kusto.ServiceLayer.QueryExecution.Contracts;
using Microsoft.Kusto.ServiceLayer.QueryExecution.DataStorage;
using Microsoft.SqlTools.Utility;
using System.Globalization;
namespace Microsoft.Kusto.ServiceLayer.QueryExecution
{
/// <summary>
/// This class represents a batch within a query
/// </summary>
public class Batch : IDisposable
{
#region Member Variables
/// <summary>
/// For IDisposable implementation, whether or not this has been disposed
/// </summary>
private bool disposed;
/// <summary>
/// Local time when the execution and retrieval of files is finished
/// </summary>
private DateTime executionEndTime;
/// <summary>
/// Local time when the execution starts, specifically when the object is created
/// </summary>
private DateTime executionStartTime;
/// <summary>
/// Whether or not any messages have been sent
/// </summary>
private bool messagesSent;
/// <summary>
/// Factory for creating readers/writers for the output of the batch
/// </summary>
private readonly IFileStreamFactory outputFileFactory;
/// <summary>
/// Internal representation of the result sets so we can modify internally
/// </summary>
private readonly List<ResultSet> resultSets;
/// <summary>
/// Special action which this batch performed
/// </summary>
private readonly SpecialAction specialAction;
/// <summary>
/// Flag indicating whether a separate KeyInfo query should be run
/// to get the full ColumnSchema metadata.
/// </summary>
private readonly bool getFullColumnSchema;
#endregion
internal Batch(string batchText, SelectionData selection, int ordinalId,
IFileStreamFactory outputFileFactory, int executionCount = 1, bool getFullColumnSchema = false)
{
// 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<ResultSet>();
this.outputFileFactory = outputFileFactory;
specialAction = new SpecialAction();
BatchExecutionCount = executionCount > 0 ? executionCount : 1;
this.getFullColumnSchema = getFullColumnSchema;
}
#region Events
/// <summary>
/// Asynchronous handler for when batches are completed
/// </summary>
/// <param name="batch">The batch that completed</param>
public delegate Task BatchAsyncEventHandler(Batch batch);
/// <summary>
/// Asynchronous handler for when a message is emitted by the sql connection
/// </summary>
/// <param name="message">The message that was emitted</param>
public delegate Task BatchAsyncMessageHandler(ResultMessage message);
/// <summary>
/// Event that will be called when the batch has completed execution
/// </summary>
public event BatchAsyncEventHandler BatchCompletion;
/// <summary>
/// Event that will be called when a message has been emitted
/// </summary>
public event BatchAsyncMessageHandler BatchMessageSent;
/// <summary>
/// Event to call when the batch has started execution
/// </summary>
public event BatchAsyncEventHandler BatchStart;
/// <summary>
/// Event that will be called when the resultset has completed execution. It will not be
/// called from the Batch but from the ResultSet instance.
/// </summary>
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompletion;
/// <summary>
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results. It will not be
/// called from the Batch but from the ResultSet instance.
/// </summary>
public event ResultSet.ResultSetAsyncEventHandler ResultSetAvailable;
/// <summary>
/// Event that will be called when additional rows in the result set are available (rowCount available has increased). It will not be
/// called from the Batch but from the ResultSet instance.
/// </summary>
public event ResultSet.ResultSetAsyncEventHandler ResultSetUpdated;
#endregion
#region Properties
/// <summary>
/// The text of batch that will be executed
/// </summary>
public string BatchText { get; set; }
public int BatchExecutionCount { get; private set; }
/// <summary>
/// Localized timestamp for when the execution completed.
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
/// </summary>
public string ExecutionEndTimeStamp => executionEndTime.ToString("o");
/// <summary>
/// Localized timestamp for how long it took for the execution to complete
/// </summary>
public string ExecutionElapsedTime
{
get
{
TimeSpan elapsedTime = executionEndTime - executionStartTime;
return elapsedTime.ToString();
}
}
/// <summary>
/// Localized timestamp for when the execution began.
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
/// </summary>
public string ExecutionStartTimeStamp => executionStartTime.ToString("o");
/// <summary>
/// Whether or not this batch encountered an error that halted execution
/// </summary>
public bool HasError { get; set; }
/// <summary>
/// Whether or not this batch has been executed, regardless of success or failure
/// </summary>
public bool HasExecuted { get; set; }
/// <summary>
/// Ordinal of the batch in the query
/// </summary>
public int Id { get; }
/// <summary>
/// The result sets of the batch execution
/// </summary>
public IList<ResultSet> ResultSets => resultSets;
/// <summary>
/// Property for generating a set result set summaries from the result sets
/// </summary>
public ResultSetSummary[] ResultSummaries
{
get
{
lock (resultSets)
{
return resultSets.Select(set => set.Summary).ToArray();
}
}
}
/// <summary>
/// Creates a <see cref="BatchSummary"/> based on the batch instance
/// </summary>
public BatchSummary Summary
{
get
{
// Batch summary with information available at start
BatchSummary summary = new BatchSummary
{
Id = Id,
Selection = Selection,
ExecutionStart = ExecutionStartTimeStamp,
HasError = HasError
};
// Add on extra details if we finished executing it
if (HasExecuted)
{
summary.ResultSetSummaries = ResultSummaries;
summary.ExecutionEnd = ExecutionEndTimeStamp;
summary.ExecutionElapsed = ExecutionElapsedTime;
summary.SpecialAction = ProcessResultSetSpecialActions();
}
return summary;
}
}
/// <summary>
/// The range from the file that is this batch
/// </summary>
internal SelectionData Selection { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Executes this batch and captures any server messages that are returned.
/// </summary>
/// <param name="conn">The connection to use to execute the batch</param>
/// <param name="cancellationToken">Token for cancelling the execution</param>
public async Task Execute(ReliableDataSourceConnection conn, CancellationToken cancellationToken)
{
// Sanity check to make sure we haven't already run this batch
if (HasExecuted)
{
throw new InvalidOperationException("Batch has already executed.");
}
// Notify that we've started execution
if (BatchStart != null)
{
await BatchStart(this);
}
try
{
await DoExecute(conn, cancellationToken);
}
catch (TaskCanceledException)
{
// Cancellation isn't considered an error condition
await SendMessage(SR.QueryServiceQueryCancelled, false);
throw;
}
catch (Exception e)
{
HasError = true;
await SendMessage(SR.QueryServiceQueryFailed(e.Message), true);
throw;
}
finally
{
// 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);
}
}
}
private async Task DoExecute(ReliableDataSourceConnection conn, CancellationToken cancellationToken)
{
bool canContinue = true;
int timesLoop = this.BatchExecutionCount;
await SendMessageIfExecutingMultipleTimes(SR.EE_ExecutionInfo_InitializingLoop, false);
executionStartTime = DateTime.Now;
while (canContinue && timesLoop > 0)
{
try
{
await ExecuteOnce(conn, cancellationToken);
}
catch (DbException dbe)
{
HasError = true;
canContinue = await UnwrapDbException(dbe);
if (canContinue)
{
// If it's a multi-batch, we notify the user that we're ignoring a single failure.
await SendMessageIfExecutingMultipleTimes(SR.EE_BatchExecutionError_Ignoring, false);
}
}
timesLoop--;
}
await SendMessageIfExecutingMultipleTimes(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_FinalizingLoop, this.BatchExecutionCount), false);
}
private async Task SendMessageIfExecutingMultipleTimes(string message, bool isError)
{
if (IsExecutingMultipleTimes())
{
await SendMessage(message, isError);
}
}
private bool IsExecutingMultipleTimes()
{
return this.BatchExecutionCount > 1;
}
private async Task ExecuteOnce(ReliableDataSourceConnection conn, CancellationToken cancellationToken)
{
// Make sure we haven't cancelled yet
cancellationToken.ThrowIfCancellationRequested();
ConnectionService.EnsureConnectionIsOpen(conn);
// Execute the command to get back a reader
using (IDataReader reader = await conn.GetUnderlyingConnection().ExecuteQueryAsync(BatchText, cancellationToken, conn.Database))
{
do
{
// Verify that the cancellation token hasn't been canceled
cancellationToken.ThrowIfCancellationRequested();
// This resultset has results (i.e. SELECT/etc queries)
ResultSet resultSet = new ResultSet(resultSets.Count, Id, outputFileFactory);
resultSet.ResultAvailable += ResultSetAvailable;
resultSet.ResultUpdated += ResultSetUpdated;
resultSet.ResultCompletion += ResultSetCompletion;
// Add the result set to the results of the query
lock (resultSets)
{
resultSets.Add(resultSet);
}
// Read until we hit the end of the result set
await resultSet.ReadResultToEnd(reader, cancellationToken);
} while (reader.NextResult());
// If there were no messages, for whatever reason (NO COUNT set, messages
// were emitted, records returned), output a "successful" message
if (!messagesSent)
{
await SendMessage(SR.QueryServiceCompletedSuccessfully, false);
}
}
}
/// <summary>
/// Generates a subset of the rows from a result set of the batch
/// </summary>
/// <param name="resultSetIndex">The index for selecting the result set</param>
/// <param name="startRow">The starting row of the results</param>
/// <param name="rowCount">How many rows to retrieve</param>
/// <returns>A subset of results</returns>
public Task<ResultSetSubset> GetSubset(int resultSetIndex, long startRow, int rowCount)
{
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.GetSubset(startRow, rowCount);
}
/// <summary>
/// Generates an execution plan
/// </summary>
/// <param name="resultSetIndex">The index for selecting the result set</param>
/// <returns>An execution plan object</returns>
public Task<ExecutionPlan> 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();
}
/// <summary>
/// Saves a result to a file format selected by the user
/// </summary>
/// <param name="saveParams">Parameters for the save as request</param>
/// <param name="fileFactory">
/// Factory for creating the reader/writer pair for outputing to the selected format
/// </param>
/// <param name="successHandler">Delegate to call when request successfully completes</param>
/// <param name="failureHandler">Delegate to call if the request fails</param>
public void SaveAs(SaveResultsRequestParams saveParams, IFileStreamFactory fileFactory,
ResultSet.SaveAsAsyncEventHandler successHandler, ResultSet.SaveAsFailureAsyncEventHandler failureHandler)
{
// Get the result set to save
ResultSet resultSet;
lock (resultSets)
{
// Sanity check to make sure we have a valid result set
if (saveParams.ResultSetIndex < 0 || saveParams.ResultSetIndex >= resultSets.Count)
{
throw new ArgumentOutOfRangeException(nameof(saveParams.BatchIndex), SR.QueryServiceSubsetResultSetOutOfRange);
}
resultSet = resultSets[saveParams.ResultSetIndex];
}
resultSet.SaveAs(saveParams, fileFactory, successHandler, failureHandler);
}
#endregion
#region Private Helpers
private async Task SendMessage(string message, bool isError)
{
// If the message event is null, this is a no-op
if (BatchMessageSent == null)
{
return;
}
// State that we've sent any message, and send it
messagesSent = true;
await BatchMessageSent(new ResultMessage(message, isError, Id));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sender">Sender of the event</param>
/// <param name="args">Arguments for the event</param>
internal void StatementCompletedHandler(object sender, StatementCompletedEventArgs args)
{
// Add a message for the number of rows the query returned
string message = args.RecordCount == 1
? SR.QueryServiceAffectedOneRow
: SR.QueryServiceAffectedRows(args.RecordCount);
SendMessage(message, false).Wait();
}
/// <summary>
/// Delegate handler for storing messages that are returned from the server
/// </summary>
/// <param name="sender">Object that fired the event</param>
/// <param name="args">Arguments from the event</param>
private async void ServerMessageHandler(object sender, SqlInfoMessageEventArgs args)
{
foreach (SqlError error in args.Errors)
{
await HandleSqlErrorMessage(error.Number, error.Class, error.State, error.LineNumber, error.Procedure, error.Message);
}
}
/// <summary>
/// Handle a single SqlError's error message by processing and displaying it. The arguments come from the error being handled
/// </summary>
internal async Task HandleSqlErrorMessage(int errorNumber, byte errorClass, byte state, int lineNumber, string procedure, string message)
{
// Did the database context change (error code 5701)?
if (errorNumber == 5701)
{
return;
}
string detailedMessage;
if (string.IsNullOrEmpty(procedure))
{
detailedMessage = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
errorNumber, errorClass, state, lineNumber + (Selection != null ? Selection.StartLine : 0),
Environment.NewLine, message);
}
else
{
detailedMessage = string.Format("Msg {0}, Level {1}, State {2}, Procedure {3}, Line {4}{5}{6}",
errorNumber, errorClass, state, procedure, lineNumber,
Environment.NewLine, message);
}
bool isError;
if (errorClass > 10)
{
isError = true;
}
else if (errorClass > 0 && errorNumber > 0)
{
isError = false;
}
else
{
isError = false;
detailedMessage = null;
}
if (detailedMessage != null)
{
await SendMessage(detailedMessage, isError);
}
else
{
await SendMessage(message, isError);
}
if (isError)
{
this.HasError = true;
}
}
/// <summary>
/// Attempts to convert an <see cref="Exception"/> to a <see cref="SqlException"/> that
/// contains much more info about Sql Server errors. The exception is then unwrapped and
/// messages are formatted and sent to the extension. If the exception cannot be
/// converted to SqlException, the message is written to the messages list.
/// </summary>
/// <param name="dbe">The exception to unwrap</param>
/// <returns>true is exception can be ignored when in a loop, false otherwise</returns>
private async Task<bool> UnwrapDbException(Exception dbe)
{
bool canIgnore = true;
SqlException se = dbe as SqlException;
if (se != null)
{
var errors = se.Errors.Cast<SqlError>().ToList();
// Detect user cancellation errors
if (errors.Any(error => error.Class == 11 && error.Number == 0))
{
// User cancellation error, add the single message
await SendMessage(SR.QueryServiceQueryCancelled, false);
canIgnore = false;
}
else
{
// Not a user cancellation error, add all
foreach (var error in errors)
{
int lineNumber = error.LineNumber + (Selection != null ? Selection.StartLine : 0);
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);
await SendMessage(message, true);
}
}
}
else
{
await SendMessage(dbe.Message, true);
}
return canIgnore;
}
/// <summary>
/// Aggregates all result sets in the batch into a single special action
/// </summary>
private SpecialAction ProcessResultSetSpecialActions()
{
foreach (ResultSet resultSet in resultSets)
{
specialAction.CombineSpecialAction(resultSet.Summary.SpecialAction);
}
return specialAction;
}
#endregion
#region IDisposable Implementation
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
lock (resultSets)
{
foreach (ResultSet r in resultSets)
{
r.Dispose();
}
}
}
disposed = true;
}
#endregion
}
}