Adding enhanced support for messages from server

* Adding error flag for Query class
* Adding message capture for messages from server (using SqlConnection
cast)
* Adding better handling of SELECT queries with 0 results
* Adding affected row count message
* Adding SqlError unwrapping (using SqlException cast)
* Removing DbException handling from QueryExecutionService and into Query
class
This commit is contained in:
Benjamin Russell
2016-08-11 15:45:59 -07:00
parent a2983539a7
commit 1be4daf41d
2 changed files with 83 additions and 28 deletions

View File

@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Data.Common; using System.Data.Common;
using System.Data.SqlClient;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -33,6 +34,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary> /// </summary>
public ConnectionInfo EditorConnection { get; set; } public ConnectionInfo EditorConnection { get; set; }
public bool HasError { get; set; }
/// <summary>
/// Whether or not the query has completed executed, regardless of success or failure
/// </summary>
public bool HasExecuted { get; set; } public bool HasExecuted { get; set; }
/// <summary> /// <summary>
@@ -40,6 +46,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary> /// </summary>
public string QueryText { get; set; } public string QueryText { get; set; }
/// <summary>
/// Messages that have come back from the server
/// </summary>
public List<string> ResultMessages { get; set; }
/// <summary> /// <summary>
/// The result sets of the query execution /// The result sets of the query execution
/// </summary> /// </summary>
@@ -71,7 +82,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
public Query(string queryText, ConnectionInfo connection) public Query(string queryText, ConnectionInfo connection)
{ {
// Sanity check for input // Sanity check for input
if (String.IsNullOrWhiteSpace(queryText)) if (String.IsNullOrEmpty(queryText))
{ {
throw new ArgumentNullException(nameof(queryText), "Query text cannot be null"); throw new ArgumentNullException(nameof(queryText), "Query text cannot be null");
} }
@@ -85,6 +96,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
EditorConnection = connection; EditorConnection = connection;
HasExecuted = false; HasExecuted = false;
ResultSets = new List<ResultSet>(); ResultSets = new List<ResultSet>();
ResultMessages = new List<string>();
cancellationSource = new CancellationTokenSource(); cancellationSource = new CancellationTokenSource();
} }
@@ -107,6 +119,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails);
using (conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) using (conn = EditorConnection.Factory.CreateSqlConnection(connectionString))
{ {
// If we have the message listener, bind to it
// TODO: This doesn't allow testing via mocking
SqlConnection sqlConn = conn as SqlConnection;
if (sqlConn != null)
{
sqlConn.InfoMessage += StoreDbMessage;
}
await conn.OpenAsync(cancellationSource.Token); await conn.OpenAsync(cancellationSource.Token);
// Create a command that we'll use for executing the query // Create a command that we'll use for executing the query
@@ -120,8 +140,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{ {
do do
{ {
// TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows // Create a message with the number of affected rows
if (!reader.HasRows) ResultMessages.Add(String.Format("({0} row(s) affected)", reader.RecordsAffected));
if (!reader.HasRows && reader.FieldCount == 0)
{ {
continue; continue;
} }
@@ -146,9 +168,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
} }
} }
} }
catch (DbException dbe)
{
HasError = true;
UnwrapDbException(dbe);
conn?.Dispose();
}
catch (Exception) catch (Exception)
{ {
// Dispose of the connection HasError = true;
conn?.Dispose(); conn?.Dispose();
throw; throw;
} }
@@ -200,6 +228,48 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}; };
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="sender">Object that fired the event</param>
/// <param name="args">Arguments from the event</param>
private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args)
{
ResultMessages.Add(args.Message);
}
/// <summary>
/// Attempts to convert a <see cref="DbException"/> to a <see cref="SqlException"/> that
/// contains much more info about Sql Server errors. The exception is then unwrapped and
/// messages are formatted and stored in <see cref="ResultMessages"/>. 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>
private void UnwrapDbException(DbException dbe)
{
SqlException se = dbe as SqlException;
if (se != null)
{
foreach (var error in se.Errors)
{
SqlError sqlError = error as SqlError;
if (sqlError != null)
{
string message = String.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
sqlError.Number, sqlError.Class, sqlError.State, sqlError.LineNumber,
Environment.NewLine, sqlError.Message);
ResultMessages.Add(message);
}
}
}
else
{
ResultMessages.Add(dbe.Message);
}
}
#region IDisposable Implementation #region IDisposable Implementation
private bool disposed; private bool disposed;

View File

@@ -242,31 +242,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
Messages = null Messages = null
}); });
try // Wait for query execution and then send back the results
await Task.WhenAll(executeTask);
QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams
{ {
// Wait for query execution and then send back the results HasError = query.HasError,
await Task.WhenAll(executeTask); Messages = query.ResultMessages.ToArray(),
QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams OwnerUri = executeParams.OwnerUri,
{ ResultSetSummaries = query.ResultSummary
HasError = false, };
Messages = new string[] { }, // TODO: Figure out how to get messages back from the server await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams);
OwnerUri = executeParams.OwnerUri,
ResultSetSummaries = query.ResultSummary
};
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams);
}
catch (DbException dbe)
{
// Dump the message to a complete event
QueryExecuteCompleteParams errorEvent = new QueryExecuteCompleteParams
{
HasError = true,
Messages = new[] {dbe.Message},
OwnerUri = executeParams.OwnerUri,
ResultSetSummaries = query.ResultSummary
};
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, errorEvent);
}
} }
#endregion #endregion