From 1be4daf41df44d1bf026bbad9a4c292637edfa31 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 11 Aug 2016 15:45:59 -0700 Subject: [PATCH] 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 --- .../QueryExecution/Query.cs | 78 ++++++++++++++++++- .../QueryExecution/QueryExecutionService.cs | 33 +++----- 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index eb093484..292b1e81 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -7,6 +7,7 @@ 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; @@ -33,6 +34,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public ConnectionInfo EditorConnection { get; set; } + public bool HasError { get; set; } + + /// + /// Whether or not the query has completed executed, regardless of success or failure + /// public bool HasExecuted { get; set; } /// @@ -40,6 +46,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public string QueryText { get; set; } + /// + /// Messages that have come back from the server + /// + public List ResultMessages { get; set; } + /// /// The result sets of the query execution /// @@ -71,7 +82,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public Query(string queryText, ConnectionInfo connection) { // Sanity check for input - if (String.IsNullOrWhiteSpace(queryText)) + if (String.IsNullOrEmpty(queryText)) { throw new ArgumentNullException(nameof(queryText), "Query text cannot be null"); } @@ -85,6 +96,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution EditorConnection = connection; HasExecuted = false; ResultSets = new List(); + ResultMessages = new List(); cancellationSource = new CancellationTokenSource(); } @@ -107,6 +119,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); 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); // Create a command that we'll use for executing the query @@ -120,8 +140,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { do { - // TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows - if (!reader.HasRows) + // Create a message with the number of affected rows + ResultMessages.Add(String.Format("({0} row(s) affected)", reader.RecordsAffected)); + + if (!reader.HasRows && reader.FieldCount == 0) { continue; } @@ -146,9 +168,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } } + catch (DbException dbe) + { + HasError = true; + UnwrapDbException(dbe); + conn?.Dispose(); + } catch (Exception) { - // Dispose of the connection + HasError = true; conn?.Dispose(); throw; } @@ -200,6 +228,48 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution }; } + /// + /// 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(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) + { + 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 private bool disposed; diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 6480e4ba..0d5f2db7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -242,31 +242,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution 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 - await Task.WhenAll(executeTask); - QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams - { - HasError = false, - Messages = new string[] { }, // TODO: Figure out how to get messages back from the server - 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); - } + HasError = query.HasError, + Messages = query.ResultMessages.ToArray(), + OwnerUri = executeParams.OwnerUri, + ResultSetSummaries = query.ResultSummary + }; + await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); } #endregion