From dee490341da8af04a993c2da573aabfa42931381 Mon Sep 17 00:00:00 2001 From: benrr101 Date: Wed, 17 Aug 2016 18:24:20 -0700 Subject: [PATCH 1/9] Stubbing out query execution settings Adding a setting for batch separator. Very small refactor to WorkspaceService that will create the basic settings upon construction of the object. --- .../QueryExecution/QueryExecutionService.cs | 16 +++++++++- .../SqlContext/QueryExecutionSettings.cs | 29 +++++++++++++++++++ .../SqlContext/SqlToolsSettings.cs | 2 ++ .../Workspace/WorkspaceService.cs | 3 +- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 5b26eafc..10f9d80b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -10,6 +10,8 @@ using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -60,6 +62,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution private readonly Lazy> queries = new Lazy>(() => new ConcurrentDictionary()); + private SqlToolsSettings Settings { get { return WorkspaceService.Instance.CurrentSettings; } } + #endregion /// @@ -81,6 +85,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution Dispose(); return Task.FromResult(0); }); + + // Register a handler for when the configuration changes + WorkspaceService.Instance.RegisterConfigChangeCallback((oldSettings, newSettings, eventContext) => + { + Settings.QueryExecutionSettings.Update(newSettings.QueryExecutionSettings); + return Task.FromResult(0); + }); } #region Request Handlers @@ -281,8 +292,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution return; } + // Retrieve the current settings for executing the query with + QueryExecutionSettings settings = WorkspaceService.Instance.CurrentSettings.QueryExecutionSettings; + // Launch the query and respond with successfully launching it - Task executeTask = query.Execute(); + Task executeTask = query.Execute(/*settings*/); await requestContext.SendResult(new QueryExecuteResult { Messages = null diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs new file mode 100644 index 00000000..7e8853a7 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.SqlContext +{ + public class QueryExecutionSettings + { + private const string DefaultBatchSeparator = "GO"; + + private string batchSeparator; + + public string BatchSeparator + { + get { return batchSeparator ?? DefaultBatchSeparator; } + set { batchSeparator = value; } + } + + /// + /// Update the current settings with the new settings + /// + /// The new settings + public void Update(QueryExecutionSettings newSettings) + { + BatchSeparator = newSettings.BatchSeparator; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs index 07ea0ffe..f5a14761 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs @@ -31,6 +31,8 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext this.ScriptAnalysis.Update(settings.ScriptAnalysis, workspaceRootPath); } } + + public QueryExecutionSettings QueryExecutionSettings { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs index f47cacb9..9cd35f19 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs @@ -44,6 +44,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace ConfigChangeCallbacks = new List(); TextDocChangeCallbacks = new List(); TextDocOpenCallbacks = new List(); + + CurrentSettings = new TConfig(); } #endregion @@ -101,7 +103,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace { // Create a workspace that will handle state for the session Workspace = new Workspace(); - CurrentSettings = new TConfig(); // Register the handlers for when changes to the workspae occur serviceHost.SetEventHandler(DidChangeTextDocumentNotification.Type, HandleDidChangeTextDocumentNotification); From 7202a7ed652806b116bcd6c74dfee02841b04488 Mon Sep 17 00:00:00 2001 From: benrr101 Date: Thu, 18 Aug 2016 17:49:16 -0700 Subject: [PATCH 2/9] WIP update to support batch processing --- .../QueryExecution/Batch.cs | 195 ++++++++++++++++++ .../QueryExecution/Contracts/BatchSummary.cs | 33 +++ .../QueryExecuteCompleteNotification.cs | 7 +- .../Contracts/ResultSetSummary.cs | 2 +- .../QueryExecution/Query.cs | 183 +++------------- 5 files changed, 264 insertions(+), 156 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs new file mode 100644 index 00000000..95a418c9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -0,0 +1,195 @@ +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.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +{ + public class Batch + { + private const string RowsAffectedFormat = "({0} row(s) affected)"; + + #region Properties + /// + /// The text of batch that will be executed + /// + public string BatchText { get; set; } + + /// + /// Whether or not the query has an error + /// + public bool HasError { get; set; } + + public bool HasExecuted { get; set; } + + /// + /// Messages that have come back from the server + /// + public List ResultMessages { get; set; } + + /// + /// The result sets of the query execution + /// + public List ResultSets { get; set; } + + /// + /// 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.Rows.Count + }).ToArray(); + } + } + + #endregion + + public Batch(string batchText) + { + // Sanity check for input + if (string.IsNullOrEmpty(batchText)) + { + throw new ArgumentNullException(nameof(batchText), "Query text cannot be null"); + } + + // Initialize the internal state + BatchText = batchText; + HasExecuted = false; + ResultSets = new List(); + ResultMessages = new List(); + } + + public async Task Execute(DbConnection conn, CancellationToken cancellationToken) + { + try + { + // Register the message listener to *this instance* of the batch + // Note: This is being done to associate messages with batches + SqlConnection sqlConn = conn as SqlConnection; + if (sqlConn != null) + { + sqlConn.InfoMessage += StoreDbMessage; + } + + // Create a command that we'll use for executing the query + using (DbCommand command = conn.CreateCommand()) + { + command.CommandText = BatchText; + command.CommandType = CommandType.Text; + + // 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 + if (!reader.HasRows && reader.FieldCount == 0) + { + // Create a message with the number of affected rows -- IF the query affects rows + ResultMessages.Add(reader.RecordsAffected >= 0 + ? string.Format(RowsAffectedFormat, reader.RecordsAffected) + : "Commad Executed Successfully"); + continue; + } + + // Read until we hit the end of the result set + ResultSet resultSet = new ResultSet(); + while (await reader.ReadAsync(cancellationToken)) + { + resultSet.AddRow(reader); + } + + // Read off the column schema information + if (reader.CanGetColumnSchema()) + { + resultSet.Columns = reader.GetColumnSchema().ToArray(); + } + + // Add the result set to the results of the query + ResultSets.Add(resultSet); + } while (await reader.NextResultAsync(cancellationToken)); + } + } + } + catch (DbException dbe) + { + HasError = true; + UnwrapDbException(dbe); + } + catch (Exception) + { + HasError = true; + throw; + } + finally + { + // Remove the message event handler from the connection + SqlConnection sqlConn = conn as SqlConnection; + if (sqlConn != null) + { + sqlConn.InfoMessage -= StoreDbMessage; + } + + // Mark that we have executed + HasExecuted = true; + } + } + + #region Private Helpers + + /// + /// 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); + } + } + + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs new file mode 100644 index 00000000..73d1d4c8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/BatchSummary.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + /// + /// Summary of a batch within a query + /// + public class BatchSummary + { + /// + /// Whether or not the batch was successful. True indicates errors, false indicates success + /// + public bool HasError { get; set; } + + /// + /// The ID of the result set within the query results + /// + public int Id { get; set; } + + /// + /// Any messages that came back from the server during execution of the batch + /// + public string[] Messages { get; set; } + + /// + /// The summaries of the result sets inside the batch + /// + public ResultSetSummary[] ResultSetSummaries { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs index f81edb62..8b6303be 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs @@ -22,15 +22,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public string[] Messages { get; set; } - /// - /// Whether or not the query was successful. True indicates errors, false indicates success - /// - public bool HasError { get; set; } - /// /// Summaries of the result sets that were returned with the query /// - public ResultSetSummary[] ResultSetSummaries { get; set; } + public BatchSummary[] BatchSummaries { get; set; } } public class QueryExecuteCompleteEvent diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs index 5f8de12a..b0a6d75c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs @@ -13,7 +13,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts public class ResultSetSummary { /// - /// The ID of the result set within the query results + /// The ID of the result set within the batch results /// public int Id { get; set; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index d9a886d4..829741af 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -5,12 +5,11 @@ 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.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; @@ -23,6 +22,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { #region Properties + /// + /// The batches underneath this query + /// + private IEnumerable Batches { get; set; } + + /// + /// The summaries of the batches underneath this query + /// + public BatchSummary[] BatchSummaries + { + get { return Batches.Select((batch, index) => new BatchSummary + { + Id = index, + HasError = batch.HasError, + Messages = batch.ResultMessages.ToArray(), + ResultSetSummaries = batch.ResultSummaries + }).ToArray(); } + } + /// /// Cancellation token source, used for cancelling async db actions /// @@ -34,47 +52,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public ConnectionInfo EditorConnection { get; set; } - /// - /// Whether or not the query has an error - /// - public bool HasError { get; set; } - /// /// Whether or not the query has completed executed, regardless of success or failure /// - public bool HasExecuted { get; set; } + public bool HasExecuted + { + get { return Batches.All(b => b.HasExecuted); } + } /// /// The text of the query to execute /// 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 - /// - public List ResultSets { get; set; } - - /// - /// Property for generating a set result set summaries from the result sets - /// - public ResultSetSummary[] ResultSummary - { - get - { - return ResultSets.Select((set, index) => new ResultSetSummary - { - ColumnInfo = set.Columns, - Id = index, - RowCount = set.Rows.Count - }).ToArray(); - } - } - #endregion /// @@ -97,10 +87,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Initialize the internal state QueryText = queryText; EditorConnection = connection; - HasExecuted = false; - ResultSets = new List(); - ResultMessages = new List(); cancellationSource = new CancellationTokenSource(); + + // Process the query into batches + ParseResult parseResult = Parser.Parse(queryText); + Batches = parseResult.Script.Batches.Select(b => new Batch(b.Sql)); } /// @@ -114,80 +105,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution throw new InvalidOperationException("Query has already executed."); } - DbConnection conn = null; - - // Create a connection from the connection details - try + // Open up a connection for querying the database + string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); + using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) { - string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); - using (conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) + // We need these to execute synchronously, otherwise the user will be very unhappy + foreach (Batch b in Batches) { - // If we have the message listener, bind to it - 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 - using (DbCommand command = conn.CreateCommand()) - { - command.CommandText = QueryText; - command.CommandType = CommandType.Text; - - // Execute the command to get back a reader - using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationSource.Token)) - { - do - { - // Create a message with the number of affected rows - if (reader.RecordsAffected >= 0) - { - ResultMessages.Add(String.Format("({0} row(s) affected)", reader.RecordsAffected)); - } - - if (!reader.HasRows && reader.FieldCount == 0) - { - continue; - } - - // Read until we hit the end of the result set - ResultSet resultSet = new ResultSet(); - while (await reader.ReadAsync(cancellationSource.Token)) - { - resultSet.AddRow(reader); - } - - // Read off the column schema information - if (reader.CanGetColumnSchema()) - { - resultSet.Columns = reader.GetColumnSchema().ToArray(); - } - - // Add the result set to the results of the query - ResultSets.Add(resultSet); - } while (await reader.NextResultAsync(cancellationSource.Token)); - } - } + await b.Execute(conn, cancellationSource.Token); } } - catch (DbException dbe) - { - HasError = true; - UnwrapDbException(dbe); - } - catch (Exception) - { - HasError = true; - throw; - } - finally - { - // Mark that we have executed - HasExecuted = true; - } } /// @@ -246,48 +173,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution cancellationSource.Cancel(); } - /// - /// 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; From f72ae9ac075b283181e4d130892eb79d5a2cdd5b Mon Sep 17 00:00:00 2001 From: benrr101 Date: Fri, 19 Aug 2016 15:22:10 -0700 Subject: [PATCH 3/9] WIP adding unit tests for batch processing --- .../QueryExecution/Batch.cs | 28 +- .../Contracts/QueryExecuteSubsetRequest.cs | 5 + .../QueryExecution/Query.cs | 40 +-- .../QueryExecution/QueryExecutionService.cs | 16 +- .../QueryExecution/ResultSet.cs | 31 ++ .../QueryExecution/CancelTests.cs | 4 +- .../QueryExecution/Common.cs | 19 +- .../QueryExecution/ExecuteTests.cs | 301 ++++++++++-------- .../QueryExecution/SubsetTests.cs | 11 +- 9 files changed, 264 insertions(+), 191 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index 95a418c9..e4b5d666 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -72,6 +72,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution 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 @@ -99,7 +105,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Create a message with the number of affected rows -- IF the query affects rows ResultMessages.Add(reader.RecordsAffected >= 0 ? string.Format(RowsAffectedFormat, reader.RecordsAffected) - : "Commad Executed Successfully"); + : "Command(s) completed successfully."); continue; } @@ -146,6 +152,26 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// 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 ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount) + { + // Sanity check to make sure we have valid numbers + if (resultSetIndex < 0 || resultSetIndex >= ResultSets.Count) + { + throw new ArgumentOutOfRangeException(nameof(resultSetIndex), "Result set index cannot be less than 0" + + "or greater than the number of result sets"); + } + + // Retrieve the result set + return ResultSets[resultSetIndex].GetSubset(startRow, rowCount); + } + #region Private Helpers /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs index cdf434bb..2c861502 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs @@ -17,6 +17,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public string OwnerUri { get; set; } + /// + /// Index of the batch to get the results from + /// + public int BatchIndex { get; set; } + /// /// Index of the result set to get the results from /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 829741af..2470dd11 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -25,7 +26,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// The batches underneath this query /// - private IEnumerable Batches { get; set; } + private Batch[] Batches { get; set; } /// /// The summaries of the batches underneath this query @@ -72,7 +73,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// The text of the query to execute /// The information of the connection to use to execute the query - public Query(string queryText, ConnectionInfo connection) + /// Settings for how to execute the query, from the user + public Query(string queryText, ConnectionInfo connection, QueryExecutionSettings settings) { // Sanity check for input if (String.IsNullOrEmpty(queryText)) @@ -90,8 +92,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution cancellationSource = new CancellationTokenSource(); // Process the query into batches - ParseResult parseResult = Parser.Parse(queryText); - Batches = parseResult.Script.Batches.Select(b => new Batch(b.Sql)); + ParseResult parseResult = Parser.Parse(queryText, new ParseOptions + { + BatchSeparator = settings.BatchSeparator + }); + Batches = parseResult.Script.Batches.Select(b => new Batch(b.Sql)).ToArray(); } /// @@ -120,11 +125,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// Retrieves a subset of the result sets /// + /// The index for selecting the batch item /// The index for selecting the result set /// The starting row of the results /// How many rows to retrieve /// A subset of results - public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount) + public ResultSetSubset GetSubset(int batchIndex, int resultSetIndex, int startRow, int rowCount) { // Sanity check that the results are available if (!HasExecuted) @@ -132,30 +138,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution throw new InvalidOperationException("The query has not completed, yet."); } - // Sanity check to make sure we have valid numbers - if (resultSetIndex < 0 || resultSetIndex >= ResultSets.Count) + // Sanity check to make sure that the batch is within bounds + if (batchIndex < 0 || batchIndex >= Batches.Length) { - throw new ArgumentOutOfRangeException(nameof(resultSetIndex), "Result set index cannot be less than 0" + + throw new ArgumentOutOfRangeException(nameof(batchIndex), "Result set index cannot be less than 0" + "or greater than the number of result sets"); } - ResultSet targetResultSet = ResultSets[resultSetIndex]; - if (startRow < 0 || startRow >= targetResultSet.Rows.Count) - { - throw new ArgumentOutOfRangeException(nameof(startRow), "Start row cannot be less than 0 " + - "or greater than the number of rows in the resultset"); - } - if (rowCount <= 0) - { - throw new ArgumentOutOfRangeException(nameof(rowCount), "Row count must be a positive integer"); - } - // Retrieve the subset of the results as per the request - object[][] rows = targetResultSet.Rows.Skip(startRow).Take(rowCount).ToArray(); - return new ResultSetSubset - { - Rows = rows, - RowCount = rows.Length - }; + return Batches[batchIndex].GetSubset(resultSetIndex, startRow, rowCount); } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 10f9d80b..f23925ab 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -134,7 +134,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution var result = new QueryExecuteSubsetResult { Message = null, - ResultSubset = query.GetSubset( + ResultSubset = query.GetSubset(subsetParams.BatchIndex, subsetParams.ResultSetIndex, subsetParams.RowsStartIndex, subsetParams.RowsCount) }; await requestContext.SendResult(result); @@ -262,8 +262,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery); } + // Retrieve the current settings for executing the query with + QueryExecutionSettings settings = WorkspaceService.Instance.CurrentSettings.QueryExecutionSettings; + // If we can't add the query now, it's assumed the query is in progress - Query newQuery = new Query(executeParams.QueryText, connectionInfo); + Query newQuery = new Query(executeParams.QueryText, connectionInfo, settings); if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) { await requestContext.SendResult(new QueryExecuteResult @@ -292,11 +295,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution return; } - // Retrieve the current settings for executing the query with - QueryExecutionSettings settings = WorkspaceService.Instance.CurrentSettings.QueryExecutionSettings; - // Launch the query and respond with successfully launching it - Task executeTask = query.Execute(/*settings*/); + Task executeTask = query.Execute(); await requestContext.SendResult(new QueryExecuteResult { Messages = null @@ -306,10 +306,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution await Task.WhenAll(executeTask); QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams { - HasError = query.HasError, - Messages = query.ResultMessages.ToArray(), OwnerUri = executeParams.OwnerUri, - ResultSetSummaries = query.ResultSummary + BatchSummaries = query.BatchSummaries }; await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs index fed08ea3..058dc54c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs @@ -3,8 +3,11 @@ // 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.Common; +using System.Linq; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -33,5 +36,33 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } Rows.Add(row.ToArray()); } + + /// + /// Generates a subset of the rows from the result set + /// + /// The starting row of the results + /// How many rows to retrieve + /// A subset of results + public ResultSetSubset GetSubset(int startRow, int rowCount) + { + // Sanity check to make sure that the row and the row count are within bounds + if (startRow < 0 || startRow >= Rows.Count) + { + throw new ArgumentOutOfRangeException(nameof(startRow), "Start row cannot be less than 0 " + + "or greater than the number of rows in the resultset"); + } + if (rowCount <= 0) + { + throw new ArgumentOutOfRangeException(nameof(rowCount), "Row count must be a positive integer"); + } + + // Retrieve the subset of the results as per the request + object[][] rows = Rows.Skip(startRow).Take(rowCount).ToArray(); + return new ResultSetSubset + { + Rows = rows, + RowCount = rows.Length + }; + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs index dbc344f8..4b9dc39a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class CancelTests { - [Fact] + //[Fact] public void CancelInProgressQueryTest() { // If: @@ -23,7 +23,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution var executeParams = new QueryExecuteParams { QueryText = "Doesn't Matter", OwnerUri = Common.OwnerUri }; var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); - queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Fake that it hasn't completed execution + //queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Fake that it hasn't completed execution // ... And then I request to cancel the query var cancelParams = new QueryCancelParams {OwnerUri = Common.OwnerUri}; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index f887b50f..ae2bf49f 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Data.SqlClient; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; @@ -10,6 +9,7 @@ using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Moq; using Moq.Protected; @@ -20,14 +20,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public const string OwnerUri = "testFile"; - public static readonly Dictionary[] StandardTestData = + public const int StandardRows = 5; + + public const int StandardColumns = 5; + + public static Dictionary[] StandardTestData { - new Dictionary { {"col1", "val11"}, { "col2", "val12"}, { "col3", "val13"}, { "col4", "col14"} }, - new Dictionary { {"col1", "val21"}, { "col2", "val22"}, { "col3", "val23"}, { "col4", "col24"} }, - new Dictionary { {"col1", "val31"}, { "col2", "val32"}, { "col3", "val33"}, { "col4", "col34"} }, - new Dictionary { {"col1", "val41"}, { "col2", "val42"}, { "col3", "val43"}, { "col4", "col44"} }, - new Dictionary { {"col1", "val51"}, { "col2", "val52"}, { "col3", "val53"}, { "col4", "col54"} }, - }; + get { return GetTestData(StandardRows, StandardColumns); } + } public static Dictionary[] GetTestData(int columns, int rows) { @@ -47,7 +47,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public static Query GetBasicExecutedQuery() { - Query query = new Query("SIMPLE QUERY", CreateTestConnectionInfo(new[] { StandardTestData }, false)); + ConnectionInfo ci = CreateTestConnectionInfo(new[] {StandardTestData}, false); + Query query = new Query("SIMPLE QUERY", ci, new QueryExecutionSettings()); query.Execute().Wait(); return query; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index 1cc56e53..e9dd96e3 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,9 +1,13 @@ using System; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; using Moq; using Xunit; @@ -11,193 +15,194 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class ExecuteTests { - #region Query Class Tests + #region Batch Class Tests [Fact] - public void QueryCreationTest() + public void BatchCreationTest() { - // If I create a new query... - Query query = new Query("NO OP", Common.CreateTestConnectionInfo(null, false)); + // If I create a new batch... + Batch batch = new Batch("NO OP"); // Then: - // ... It should not have executed - Assert.False(query.HasExecuted, "The query should not have executed."); + // ... The text of the batch should be stored + Assert.NotEmpty(batch.BatchText); + + // ... It should not have executed and no error + Assert.False(batch.HasExecuted, "The query should not have executed."); + Assert.False(batch.HasError, "The batch should not have an error"); // ... The results should be empty - Assert.Empty(query.ResultSets); - Assert.Empty(query.ResultSummary); + Assert.Empty(batch.ResultSets); + Assert.Empty(batch.ResultSummaries); + Assert.Empty(batch.ResultMessages); } [Fact] - public void QueryExecuteNoResultSets() + public void BatchExecuteNoResultSets() { // If I execute a query that should get no result sets - Query query = new Query("Query with no result sets", Common.CreateTestConnectionInfo(null, false)); - query.Execute().Wait(); + Batch batch = new Batch("Query with no result sets"); + batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None).Wait(); // Then: // ... It should have executed without error - Assert.True(query.HasExecuted, "The query should have been marked executed."); - Assert.False(query.HasError); - + Assert.True(batch.HasExecuted, "The query should have been marked executed."); + Assert.False(batch.HasError, "The batch should not have an error"); + // ... The results should be empty - Assert.Empty(query.ResultSets); - Assert.Empty(query.ResultSummary); + Assert.Empty(batch.ResultSets); + Assert.Empty(batch.ResultSummaries); // ... The results should not be null - Assert.NotNull(query.ResultSets); - Assert.NotNull(query.ResultSummary); + Assert.NotNull(batch.ResultSets); + Assert.NotNull(batch.ResultSummaries); // ... There should be a message for how many rows were affected - Assert.Equal(1, query.ResultMessages.Count); + Assert.Equal(1, batch.ResultMessages.Count); } [Fact] - public void QueryExecuteQueryOneResultSet() + public void BatchExecuteOneResultSet() { - ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] {Common.StandardTestData}, false); + int resultSets = 1; + ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); // If I execute a query that should get one result set - int resultSets = 1; - int rows = 5; - int columns = 4; - Query query = new Query("Query with one result sets", ci); - query.Execute().Wait(); + Batch batch = new Batch("Query with one result sets"); + batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: // ... It should have executed without error - Assert.True(query.HasExecuted, "The query should have been marked executed."); - Assert.False(query.HasError); + Assert.True(batch.HasExecuted, "The batch should have been marked executed."); + Assert.False(batch.HasError, "The batch should not have an error"); // ... There should be exactly one result set - Assert.Equal(resultSets, query.ResultSets.Count); + Assert.Equal(resultSets, batch.ResultSets.Count); + Assert.Equal(resultSets, batch.ResultSummaries.Length); // ... Inside the result set should be with 5 rows - Assert.Equal(rows, query.ResultSets[0].Rows.Count); + Assert.Equal(Common.StandardRows, batch.ResultSets[0].Rows.Count); + Assert.Equal(Common.StandardRows, batch.ResultSummaries[0].RowCount); // ... Inside the result set should have 5 columns and 5 column definitions - Assert.Equal(columns, query.ResultSets[0].Rows[0].Length); - Assert.Equal(columns, query.ResultSets[0].Columns.Length); - - // ... There should be exactly one result set summary - Assert.Equal(resultSets, query.ResultSummary.Length); - - // ... Inside the result summary, there should be 5 column definitions - Assert.Equal(columns, query.ResultSummary[0].ColumnInfo.Length); - - // ... Inside the result summary, there should be 5 rows - Assert.Equal(rows, query.ResultSummary[0].RowCount); + Assert.Equal(Common.StandardColumns, batch.ResultSets[0].Rows[0].Length); + Assert.Equal(Common.StandardColumns, batch.ResultSets[0].Columns.Length); + Assert.Equal(Common.StandardColumns, batch.ResultSummaries[0].ColumnInfo.Length); // ... There should be a message for how many rows were affected - Assert.Equal(resultSets, query.ResultMessages.Count); + Assert.Equal(resultSets, batch.ResultMessages.Count); } [Fact] - public void QueryExecuteQueryTwoResultSets() + public void BatchExecuteTwoResultSets() { - var dataset = new[] {Common.StandardTestData, Common.StandardTestData}; + var dataset = new[] { Common.StandardTestData, Common.StandardTestData }; int resultSets = dataset.Length; - int rows = Common.StandardTestData.Length; - int columns = Common.StandardTestData[0].Count; ConnectionInfo ci = Common.CreateTestConnectionInfo(dataset, false); // If I execute a query that should get two result sets - Query query = new Query("Query with two result sets", ci); - query.Execute().Wait(); + Batch batch = new Batch("Query with two result sets"); + batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: // ... It should have executed without error - Assert.True(query.HasExecuted, "The query should have been marked executed."); - Assert.False(query.HasError); + Assert.True(batch.HasExecuted, "The batch should have been marked executed."); + Assert.False(batch.HasError, "The batch should not have an error"); // ... There should be exactly two result sets - Assert.Equal(resultSets, query.ResultSets.Count); + Assert.Equal(resultSets, batch.ResultSets.Count); - foreach (ResultSet rs in query.ResultSets) + foreach (ResultSet rs in batch.ResultSets) { // ... Each result set should have 5 rows - Assert.Equal(rows, rs.Rows.Count); + Assert.Equal(Common.StandardRows, rs.Rows.Count); // ... Inside each result set should be 5 columns and 5 column definitions - Assert.Equal(columns, rs.Rows[0].Length); - Assert.Equal(columns, rs.Columns.Length); + Assert.Equal(Common.StandardColumns, rs.Rows[0].Length); + Assert.Equal(Common.StandardColumns, rs.Columns.Length); } // ... There should be exactly two result set summaries - Assert.Equal(resultSets, query.ResultSummary.Length); + Assert.Equal(resultSets, batch.ResultSummaries.Length); - foreach (ResultSetSummary rs in query.ResultSummary) + foreach (ResultSetSummary rs in batch.ResultSummaries) { - // ... Inside each result summary, there should be 5 column definitions - Assert.Equal(columns, rs.ColumnInfo.Length); - // ... Inside each result summary, there should be 5 rows - Assert.Equal(rows, rs.RowCount); + Assert.Equal(Common.StandardRows, rs.RowCount); + + // ... Inside each result summary, there should be 5 column definitions + Assert.Equal(Common.StandardColumns, rs.ColumnInfo.Length); } // ... There should be a message for how many rows were affected - Assert.Equal(resultSets, query.ResultMessages.Count); + Assert.Equal(resultSets, batch.ResultMessages.Count); } [Fact] - public void QueryExecuteInvalidQuery() + public void BatchExecuteInvalidQuery() { ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); - // If I execute a query that is invalid - Query query = new Query("Invalid query", ci); - query.Execute().Wait(); + // If I execute a batch that is invalid + Batch batch = new Batch("Invalid query"); + batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: // ... It should have executed with error - Assert.True(query.HasExecuted); - Assert.True(query.HasError); - - // ... There should be plenty of messages for the eror - Assert.NotEmpty(query.ResultMessages); + Assert.True(batch.HasExecuted); + Assert.True(batch.HasError); + + // ... There should be no result sets + Assert.Empty(batch.ResultSets); + Assert.Empty(batch.ResultSummaries); + + // ... There should be plenty of messages for the error + Assert.NotEmpty(batch.ResultMessages); } [Fact] - public void QueryExecuteExecutedQuery() + public async Task BatchExecuteExecuted() { - ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] {Common.StandardTestData}, false); + ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); - // If I execute a query - Query query = new Query("Any query", ci); - query.Execute().Wait(); + // If I execute a batch + Batch batch = new Batch("Any query"); + batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: // ... It should have executed without error - Assert.True(query.HasExecuted, "The query should have been marked executed."); - Assert.False(query.HasError); + Assert.True(batch.HasExecuted, "The batch should have been marked executed."); + Assert.False(batch.HasError, "The batch should not have an error"); // If I execute it again // Then: - // ... It should throw an invalid operation exception wrapped in an aggregate exception - AggregateException ae = Assert.Throws(() => query.Execute().Wait()); - Assert.Equal(1, ae.InnerExceptions.Count); - Assert.IsType(ae.InnerExceptions[0]); + // ... It should throw an invalid operation exception + await Assert.ThrowsAsync(() => + batch.Execute(GetConnection(ci), CancellationToken.None)); // ... The data should still be available without error - Assert.False(query.HasError); - Assert.True(query.HasExecuted, "The query should still be marked executed."); - Assert.NotEmpty(query.ResultSets); - Assert.NotEmpty(query.ResultSummary); + Assert.False(batch.HasError, "The batch should not be in an error condition"); + Assert.True(batch.HasExecuted, "The batch should still be marked executed."); + Assert.NotEmpty(batch.ResultSets); + Assert.NotEmpty(batch.ResultSummaries); } [Theory] [InlineData("")] - [InlineData(" ")] [InlineData(null)] - public void QueryExecuteNoQuery(string query) + public void BatchExecuteNoSql(string query) { // If: - // ... I create a query that has an empty query + // ... I create a batch that has an empty query // Then: // ... It should throw an exception - Assert.Throws(() => new Query(query, null)); + Assert.Throws(() => new Batch(query)); } + #endregion + + #region Query Class Tests + [Fact] public void QueryExecuteNoConnectionInfo() { @@ -205,14 +210,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... I create a query that has a null connection info // Then: // ... It should throw an exception - Assert.Throws(() => new Query("Some Query", null)); + Assert.Throws(() => new Query("Some Query", null, new QueryExecutionSettings())); + } + + [Fact] + public void QueryExecuteNoSettings() + { + // If: + // ... I create a query that has a null settings + // Then: + // ... It should throw an exception + Assert.Throws(() => + new Query("Some query", Common.CreateTestConnectionInfo(null, false), null)); } #endregion #region Service Tests - [Fact] + //[Fact] public void QueryExecuteValidNoResultsTest() { // If: @@ -230,15 +246,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... A successful result should have been sent with messages // ... A completion event should have been fired with empty results // ... There should be one active query - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.NotEmpty(completeParams.Messages); - Assert.Empty(completeParams.ResultSetSummaries); - Assert.False(completeParams.HasError); - Assert.Equal(1, queryService.ActiveQueries.Count); + //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + //Assert.Null(result.Messages); + //Assert.NotEmpty(completeParams.Messages); + //Assert.Equal(1, completeParams.BatchSummaries); + //Assert.True(completeParams.); + //Assert.Equal(1, queryService.ActiveQueries.Count); } - [Fact] + //[Fact] public void QueryExecuteValidResultsTest() { // If: @@ -256,15 +272,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... A successful result should have been sent with messages // ... A completion event should have been fired with one result // ... There should be one active query - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.NotEmpty(completeParams.Messages); - Assert.NotEmpty(completeParams.ResultSetSummaries); - Assert.False(completeParams.HasError); - Assert.Equal(1, queryService.ActiveQueries.Count); + //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + //Assert.Null(result.Messages); + //Assert.NotEmpty(completeParams.Messages); + //Assert.NotEmpty(completeParams.ResultSetSummaries); + //Assert.False(completeParams.HasError); + //Assert.Equal(1, queryService.ActiveQueries.Count); } - [Fact] + //[Fact] public void QueryExecuteUnconnectedUriTest() { // If: @@ -281,13 +297,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No completion event should have been fired // ... No error event should have been fired // ... There should be no active queries - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); - Assert.NotNull(result.Messages); - Assert.NotEmpty(result.Messages); - Assert.Empty(queryService.ActiveQueries); + //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + //Assert.NotNull(result.Messages); + //Assert.NotEmpty(result.Messages); + //Assert.Empty(queryService.ActiveQueries); } - [Fact] + //[Fact] public void QueryExecuteInProgressTest() { // If: @@ -300,23 +316,23 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution queryService.HandleExecuteRequest(queryParams, firstRequestContext.Object).Wait(); // ... And then I request another query without waiting for the first to complete - queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished - QueryExecuteResult result = null; - var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); - queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); + //queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished + //QueryExecuteResult result = null; + //var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + //queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); - // Then: - // ... No errors should have been sent - // ... A result should have been sent with an error message - // ... No completion event should have been fired - // ... There should only be one active query - VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.AtMostOnce(), Times.Never()); - Assert.NotNull(result.Messages); - Assert.NotEmpty(result.Messages); - Assert.Equal(1, queryService.ActiveQueries.Count); + //// Then: + //// ... No errors should have been sent + //// ... A result should have been sent with an error message + //// ... No completion event should have been fired + //// ... There should only be one active query + //VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.AtMostOnce(), Times.Never()); + //Assert.NotNull(result.Messages); + //Assert.NotEmpty(result.Messages); + //Assert.Equal(1, queryService.ActiveQueries.Count); } - [Fact] + //[Fact] public void QueryExecuteCompletedTest() { // If: @@ -338,15 +354,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A result should have been sent with no errors // ... There should only be one active query - VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.False(complete.HasError); - Assert.Equal(1, queryService.ActiveQueries.Count); + //VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Never()); + //Assert.Null(result.Messages); + //Assert.False(complete.HasError); + //Assert.Equal(1, queryService.ActiveQueries.Count); } - [Theory] - [InlineData("")] - [InlineData(null)] + //[Theory] + //[InlineData("")] + //[InlineData(null)] public void QueryExecuteMissingQueryTest(string query) { // If: @@ -362,12 +378,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A result should have been sent with an error message // ... No completion event should have been fired - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); - Assert.NotNull(result.Messages); - Assert.NotEmpty(result.Messages); + //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + //Assert.NotNull(result.Messages); + //Assert.NotEmpty(result.Messages); } - [Fact] + //[Fact] public void QueryExecuteInvalidQueryTest() { // If: @@ -384,10 +400,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A result should have been sent with success (we successfully started the query) // ... A completion event should have been sent with error - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.True(complete.HasError); - Assert.NotEmpty(complete.Messages); + //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + //Assert.Null(result.Messages); + //Assert.True(complete.HasError); + //Assert.NotEmpty(complete.Messages); } #endregion @@ -400,5 +416,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution It.IsAny()), sendEventCalls); mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); } + + private DbConnection GetConnection(ConnectionInfo info) + { + return info.Factory.CreateSqlConnection(ConnectionService.BuildConnectionString(info.ConnectionDetails)); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index bdb0dc48..84c4701e 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; using Moq; using Xunit; @@ -21,7 +22,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Query q = Common.GetBasicExecutedQuery(); // ... And I ask for a subset with valid arguments - ResultSetSubset subset = q.GetSubset(0, 0, rowCount); + ResultSetSubset subset = q.GetSubset(0, 0, 0, rowCount); // Then: // I should get the requested number of rows @@ -33,12 +34,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public void SubsetUnexecutedQueryTest() { // If I have a query that has *not* been executed - Query q = new Query("NO OP", Common.CreateTestConnectionInfo(null, false)); + Query q = new Query("NO OP", Common.CreateTestConnectionInfo(null, false), new QueryExecutionSettings()); // ... And I ask for a subset with valid arguments // Then: // ... It should throw an exception - Assert.Throws(() => q.GetSubset(0, 0, 2)); + Assert.Throws(() => q.GetSubset(0, 0, 0, 2)); } [Theory] @@ -56,7 +57,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... And I ask for a subset with an invalid result set index // Then: // ... It should throw an exception - Assert.Throws(() => q.GetSubset(resultSetIndex, rowStartInex, rowCount)); + Assert.Throws(() => q.GetSubset(0, resultSetIndex, rowStartInex, rowCount)); } #endregion @@ -119,7 +120,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri }; var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); - queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; + //queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // ... And I then ask for a valid set of results from it var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; From c719ed45982a607cb2d96cded6199c7b703015f3 Mon Sep 17 00:00:00 2001 From: benrr101 Date: Fri, 19 Aug 2016 15:42:05 -0700 Subject: [PATCH 4/9] Moving to official moq version --- test/Microsoft.SqlTools.ServiceLayer.Test/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json index 23c97d0b..882f0af5 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json @@ -11,7 +11,7 @@ "System.Data.SqlClient": "4.1.0", "xunit": "2.1.0", "dotnet-test-xunit": "1.0.0-rc2-192208-24", - "moq.netcore": "4.4.0-beta8", + "moq": "4.6.36-alpha", "Microsoft.SqlTools.ServiceLayer": { "target": "project" }, From 943c7b9569c1b497eed5de8df3459dadaa48a170 Mon Sep 17 00:00:00 2001 From: benrr101 Date: Fri, 19 Aug 2016 18:24:20 -0700 Subject: [PATCH 5/9] Wrapping up batch separation Adding unit tests Fixing things that got brought up from the unit tests --- .../QueryExecuteCompleteNotification.cs | 5 - .../QueryExecution/Query.cs | 49 ++-- .../SqlContext/SqlToolsSettings.cs | 1 + .../Workspace/WorkspaceService.cs | 2 +- .../QueryExecution/CancelTests.cs | 8 +- .../QueryExecution/Common.cs | 12 +- .../QueryExecution/ExecuteTests.cs | 229 +++++++++++++----- .../QueryExecution/SubsetTests.cs | 56 +++-- .../project.json | 3 +- 9 files changed, 254 insertions(+), 111 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs index 8b6303be..90c8c7b3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs @@ -17,11 +17,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public string OwnerUri { get; set; } - /// - /// Any messages that came back from the server during execution of the query - /// - public string[] Messages { get; set; } - /// /// Summaries of the result sets that were returned with the query /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 4451cc48..66243763 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -4,7 +4,6 @@ // using System; -using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Threading; @@ -21,27 +20,33 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public class Query : IDisposable { - private const string RowsAffectedFormat = "({0} row(s) affected)"; - #region Properties /// /// The batches underneath this query /// - private Batch[] Batches { get; set; } + internal Batch[] Batches { get; set; } /// /// The summaries of the batches underneath this query /// public BatchSummary[] BatchSummaries { - get { return Batches.Select((batch, index) => new BatchSummary + get { - Id = index, - HasError = batch.HasError, - Messages = batch.ResultMessages.ToArray(), - ResultSetSummaries = batch.ResultSummaries - }).ToArray(); } + if (!HasExecuted) + { + throw new InvalidOperationException("Query has not been executed."); + } + + return Batches.Select((batch, index) => new BatchSummary + { + Id = index, + HasError = batch.HasError, + Messages = batch.ResultMessages.ToArray(), + ResultSetSummaries = batch.ResultSummaries + }).ToArray(); + } } /// @@ -53,14 +58,24 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The connection info associated with the file editor owner URI, used to create a new /// connection upon execution of the query /// - public ConnectionInfo EditorConnection { get; set; } + private ConnectionInfo EditorConnection { get; set; } /// /// Whether or not the query has completed executed, regardless of success or failure /// + /// + /// Don't touch the setter unless you're doing unit tests! + /// public bool HasExecuted { get { return Batches.All(b => b.HasExecuted); } + internal set + { + foreach (var batch in Batches) + { + batch.HasExecuted = value; + } + } } /// @@ -87,6 +102,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { throw new ArgumentNullException(nameof(connection), "Connection cannot be null"); } + if (settings == null) + { + throw new ArgumentNullException(nameof(settings), "Settings cannot be null"); + } // Initialize the internal state QueryText = queryText; @@ -106,16 +125,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public async Task Execute() { - // Sanity check to make sure we haven't already run this query - if (HasExecuted) - { - throw new InvalidOperationException("Query has already executed."); - } - // Open up a connection for querying the database string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) { + await conn.OpenAsync(); + // We need these to execute synchronously, otherwise the user will be very unhappy foreach (Batch b in Batches) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs index f5a14761..198884f2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/SqlToolsSettings.cs @@ -17,6 +17,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext public SqlToolsSettings() { this.ScriptAnalysis = new ScriptAnalysisSettings(); + this.QueryExecutionSettings = new QueryExecutionSettings(); } public bool EnableProfileLoading { get; set; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs index 9cd35f19..939a4ab5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/WorkspaceService.cs @@ -54,7 +54,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace public Workspace Workspace { get; private set; } - public TConfig CurrentSettings { get; private set; } + public TConfig CurrentSettings { get; internal set; } /// /// Delegate for callbacks that occur when the configuration for the workspace changes diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs index 4b9dc39a..05df93b9 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/CancelTests.cs @@ -14,16 +14,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class CancelTests { - //[Fact] + [Fact] public void CancelInProgressQueryTest() { // If: // ... I request a query (doesn't matter what kind) and execute it var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); - var executeParams = new QueryExecuteParams { QueryText = "Doesn't Matter", OwnerUri = Common.OwnerUri }; + var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri }; var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); - //queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Fake that it hasn't completed execution + queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Fake that it hasn't completed execution // ... And then I request to cancel the query var cancelParams = new QueryCancelParams {OwnerUri = Common.OwnerUri}; @@ -46,7 +46,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // If: // ... I request a query (doesn't matter what kind) and wait for execution var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); - var executeParams = new QueryExecuteParams {QueryText = "Doesn't Matter", OwnerUri = Common.OwnerUri}; + var executeParams = new QueryExecuteParams {QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri}; var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index e9777f27..0f2b2907 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; @@ -18,6 +19,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class Common { + public const string StandardQuery = "SELECT * FROM sys.objects"; + public const string OwnerUri = "testFile"; public const int StandardRows = 5; @@ -45,10 +48,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return output; } + public static Batch GetBasicExecutedBatch() + { + Batch batch = new Batch(StandardQuery); + batch.Execute(CreateTestConnection(new[] {StandardTestData}, false), CancellationToken.None).Wait(); + return batch; + } + public static Query GetBasicExecutedQuery() { ConnectionInfo ci = CreateTestConnectionInfo(new[] {StandardTestData}, false); - Query query = new Query("SIMPLE QUERY", ci, new QueryExecutionSettings()); + Query query = new Query(StandardQuery, ci, new QueryExecutionSettings()); query.Execute().Wait(); return query; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index b26ba4a4..4f4ec505 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,5 +1,6 @@ using System; using System.Data.Common; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; @@ -8,6 +9,7 @@ using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace; using Moq; using Xunit; @@ -21,7 +23,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public void BatchCreationTest() { // If I create a new batch... - Batch batch = new Batch("NO OP"); + Batch batch = new Batch(Common.StandardQuery); // Then: // ... The text of the batch should be stored @@ -41,7 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public void BatchExecuteNoResultSets() { // If I execute a query that should get no result sets - Batch batch = new Batch("Query with no result sets"); + Batch batch = new Batch(Common.StandardQuery); batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None).Wait(); // Then: @@ -68,7 +70,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); // If I execute a query that should get one result set - Batch batch = new Batch("Query with one result sets"); + Batch batch = new Batch(Common.StandardQuery); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -101,7 +103,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(dataset, false); // If I execute a query that should get two result sets - Batch batch = new Batch("Query with two result sets"); + Batch batch = new Batch(Common.StandardQuery); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -144,7 +146,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); // If I execute a batch that is invalid - Batch batch = new Batch("Invalid query"); + Batch batch = new Batch(Common.StandardQuery); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -166,7 +168,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); // If I execute a batch - Batch batch = new Batch("Any query"); + Batch batch = new Batch(Common.StandardQuery); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -203,6 +205,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution #region Query Class Tests + [Fact] + public void QueryExecuteNoQueryText() + { + // If: + // ... I create a query that has a null query text + // Then: + // ... It should throw an exception + Assert.Throws(() => + new Query(null, Common.CreateTestConnectionInfo(null, false), new QueryExecutionSettings())); + } + [Fact] public void QueryExecuteNoConnectionInfo() { @@ -224,17 +237,105 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution new Query("Some query", Common.CreateTestConnectionInfo(null, false), null)); } + [Fact] + public void QueryExecuteSingleBatch() + { + // If: + // ... I create a query from a single batch (without separator) + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); + Query query = new Query(Common.StandardQuery, ci, new QueryExecutionSettings()); + + // Then: + // ... I should get a single batch to execute that hasn't been executed + Assert.NotEmpty(query.QueryText); + Assert.NotEmpty(query.Batches); + Assert.Equal(1, query.Batches.Length); + Assert.False(query.HasExecuted); + Assert.Throws(() => query.BatchSummaries); + + // If: + // ... I then execute the query + query.Execute().Wait(); + + // Then: + // ... The query should have completed successfully with one batch summary returned + Assert.True(query.HasExecuted); + Assert.NotEmpty(query.BatchSummaries); + Assert.Equal(1, query.BatchSummaries.Length); + } + + [Fact] + public void QueryExecuteMultipleBatches() + { + // If: + // ... I create a query from two batches (with separator) + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); + string queryText = string.Format("{0}\r\nGO\r\n{0}", Common.StandardQuery); + Query query = new Query(queryText, ci, new QueryExecutionSettings()); + + // Then: + // ... I should get back two batches to execute that haven't been executed + Assert.NotEmpty(query.QueryText); + Assert.NotEmpty(query.Batches); + Assert.Equal(2, query.Batches.Length); + Assert.False(query.HasExecuted); + Assert.Throws(() => query.BatchSummaries); + + // If: + // ... I then execute the query + query.Execute().Wait(); + + // Then: + // ... The query should have completed successfully with two batch summaries returned + Assert.True(query.HasExecuted); + Assert.NotEmpty(query.BatchSummaries); + Assert.Equal(2, query.BatchSummaries.Length); + } + + [Fact] + public void QueryExecuteInvalidBatch() + { + // If: + // ... I create a query from an invalid batch + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); + Query query = new Query("SELECT *** FROM sys.objects", ci, new QueryExecutionSettings()); + + // Then: + // ... I should get back a query with one batch not executed + Assert.NotEmpty(query.QueryText); + Assert.NotEmpty(query.Batches); + Assert.Equal(1, query.Batches.Length); + Assert.False(query.HasExecuted); + Assert.Throws(() => query.BatchSummaries); + + // If: + // ... I then execute the query + query.Execute().Wait(); + + // Then: + // ... There should be an error on the batch + Assert.True(query.HasExecuted); + Assert.NotEmpty(query.BatchSummaries); + Assert.Equal(1, query.BatchSummaries.Length); + Assert.True(query.BatchSummaries[0].HasError); + Assert.NotEmpty(query.BatchSummaries[0].Messages); + } + #endregion #region Service Tests - //[Fact] + [Fact] public void QueryExecuteValidNoResultsTest() { + // Given: + // ... Default settings are stored in the workspace service + WorkspaceService.Instance.CurrentSettings = new SqlToolsSettings(); + // If: // ... I request to execute a valid query with no results var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); - var queryParams = new QueryExecuteParams { QueryText = "Doesn't Matter", OwnerUri = Common.OwnerUri }; + var queryParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri }; QueryExecuteResult result = null; QueryExecuteCompleteParams completeParams = null; @@ -243,24 +344,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // Then: // ... No Errors should have been sent - // ... A successful result should have been sent with messages + // ... A successful result should have been sent with messages on the first batch // ... A completion event should have been fired with empty results + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Equal(1, completeParams.BatchSummaries.Length); + Assert.Empty(completeParams.BatchSummaries[0].ResultSetSummaries); + Assert.NotEmpty(completeParams.BatchSummaries[0].Messages); + // ... There should be one active query - //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - //Assert.Null(result.Messages); - //Assert.NotEmpty(completeParams.Messages); - //Assert.Equal(1, completeParams.BatchSummaries); - //Assert.True(completeParams.); - //Assert.Equal(1, queryService.ActiveQueries.Count); + Assert.Equal(1, queryService.ActiveQueries.Count); } - //[Fact] + [Fact] public void QueryExecuteValidResultsTest() { // If: // ... I request to execute a valid query with results var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(new[] { Common.StandardTestData }, false), true); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Doesn't Matter" }; + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = Common.StandardQuery }; QueryExecuteResult result = null; QueryExecuteCompleteParams completeParams = null; @@ -271,22 +373,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A successful result should have been sent with messages // ... A completion event should have been fired with one result + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Equal(1, completeParams.BatchSummaries.Length); + Assert.NotEmpty(completeParams.BatchSummaries[0].ResultSetSummaries); + Assert.NotEmpty(completeParams.BatchSummaries[0].Messages); + Assert.False(completeParams.BatchSummaries[0].HasError); + // ... There should be one active query - //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - //Assert.Null(result.Messages); - //Assert.NotEmpty(completeParams.Messages); - //Assert.NotEmpty(completeParams.ResultSetSummaries); - //Assert.False(completeParams.HasError); - //Assert.Equal(1, queryService.ActiveQueries.Count); + Assert.Equal(1, queryService.ActiveQueries.Count); } - //[Fact] + [Fact] public void QueryExecuteUnconnectedUriTest() { // If: // ... I request to execute a query using a file URI that isn't connected var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), false); - var queryParams = new QueryExecuteParams { OwnerUri = "notConnected", QueryText = "Doesn't Matter" }; + var queryParams = new QueryExecuteParams { OwnerUri = "notConnected", QueryText = Common.StandardQuery }; QueryExecuteResult result = null; var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); @@ -297,48 +401,48 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No completion event should have been fired // ... No error event should have been fired // ... There should be no active queries - //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); - //Assert.NotNull(result.Messages); - //Assert.NotEmpty(result.Messages); - //Assert.Empty(queryService.ActiveQueries); + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + Assert.Empty(queryService.ActiveQueries); } - //[Fact] + [Fact] public void QueryExecuteInProgressTest() { // If: // ... I request to execute a query var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Some Query" }; + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = Common.StandardQuery }; // Note, we don't care about the results of the first request var firstRequestContext = Common.GetQueryExecuteResultContextMock(null, null, null); queryService.HandleExecuteRequest(queryParams, firstRequestContext.Object).Wait(); // ... And then I request another query without waiting for the first to complete - //queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished - //QueryExecuteResult result = null; - //var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); - //queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); + queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // Simulate query hasn't finished + QueryExecuteResult result = null; + var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); - //// Then: - //// ... No errors should have been sent - //// ... A result should have been sent with an error message - //// ... No completion event should have been fired - //// ... There should only be one active query - //VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.AtMostOnce(), Times.Never()); - //Assert.NotNull(result.Messages); - //Assert.NotEmpty(result.Messages); - //Assert.Equal(1, queryService.ActiveQueries.Count); + // Then: + // ... No errors should have been sent + // ... A result should have been sent with an error message + // ... No completion event should have been fired + // ... There should only be one active query + VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.AtMostOnce(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + Assert.Equal(1, queryService.ActiveQueries.Count); } - //[Fact] + [Fact] public void QueryExecuteCompletedTest() { // If: // ... I request to execute a query var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Some Query" }; + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = Common.StandardQuery }; // Note, we don't care about the results of the first request var firstRequestContext = Common.GetQueryExecuteResultContextMock(null, null, null); @@ -354,15 +458,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A result should have been sent with no errors // ... There should only be one active query - //VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Never()); - //Assert.Null(result.Messages); - //Assert.False(complete.HasError); - //Assert.Equal(1, queryService.ActiveQueries.Count); + VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.False(complete.BatchSummaries.Any(b => b.HasError)); + Assert.Equal(1, queryService.ActiveQueries.Count); } - //[Theory] - //[InlineData("")] - //[InlineData(null)] + [Theory] + [InlineData("")] + [InlineData(null)] public void QueryExecuteMissingQueryTest(string query) { // If: @@ -378,21 +482,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A result should have been sent with an error message // ... No completion event should have been fired - //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); - //Assert.NotNull(result.Messages); - //Assert.NotEmpty(result.Messages); + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); // ... There should not be an active query Assert.Empty(queryService.ActiveQueries); } - //[Fact] + [Fact] public void QueryExecuteInvalidQueryTest() { // If: // ... I request to execute a query that is invalid var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, true), true); - var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = "Bad query!" }; + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = Common.StandardQuery }; QueryExecuteResult result = null; QueryExecuteCompleteParams complete = null; @@ -403,10 +507,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... No errors should have been sent // ... A result should have been sent with success (we successfully started the query) // ... A completion event should have been sent with error - //VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - //Assert.Null(result.Messages); - //Assert.True(complete.HasError); - //Assert.NotEmpty(complete.Messages); + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Equal(1, complete.BatchSummaries.Length); + Assert.True(complete.BatchSummaries[0].HasError); + Assert.NotEmpty(complete.BatchSummaries[0].Messages); } #endregion diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index 84c4701e..6549cf8b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -11,18 +11,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class SubsetTests { - #region Query Class Tests + #region Batch Class Tests [Theory] [InlineData(2)] [InlineData(20)] - public void SubsetValidTest(int rowCount) + public void BatchSubsetValidTest(int rowCount) { - // If I have an executed query - Query q = Common.GetBasicExecutedQuery(); + // If I have an executed batch + Batch b = Common.GetBasicExecutedBatch(); // ... And I ask for a subset with valid arguments - ResultSetSubset subset = q.GetSubset(0, 0, 0, rowCount); + ResultSetSubset subset = b.GetSubset(0, 0, rowCount); // Then: // I should get the requested number of rows @@ -30,18 +30,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Equal(Math.Min(rowCount, Common.StandardTestData.Length), subset.Rows.Length); } - [Fact] - public void SubsetUnexecutedQueryTest() - { - // If I have a query that has *not* been executed - Query q = new Query("NO OP", Common.CreateTestConnectionInfo(null, false), new QueryExecutionSettings()); - - // ... And I ask for a subset with valid arguments - // Then: - // ... It should throw an exception - Assert.Throws(() => q.GetSubset(0, 0, 0, 2)); - } - [Theory] [InlineData(-1, 0, 2)] // Invalid result set, too low [InlineData(2, 0, 2)] // Invalid result set, too high @@ -49,7 +37,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution [InlineData(0, 10, 2)] // Invalid start index, too high [InlineData(0, 0, -1)] // Invalid row count, too low [InlineData(0, 0, 0)] // Invalid row count, zero - public void SubsetInvalidParamsTest(int resultSetIndex, int rowStartInex, int rowCount) + public void BatchSubsetInvalidParamsTest(int resultSetIndex, int rowStartInex, int rowCount) + { + // If I have an executed batch + Batch b = Common.GetBasicExecutedBatch(); + + // ... And I ask for a subset with an invalid result set index + // Then: + // ... It should throw an exception + Assert.Throws(() => b.GetSubset(resultSetIndex, rowStartInex, rowCount)); + } + + #endregion + + #region Query Class Tests + + [Fact] + public void SubsetUnexecutedQueryTest() + { + // If I have a query that has *not* been executed + Query q = new Query(Common.StandardQuery, Common.CreateTestConnectionInfo(null, false), new QueryExecutionSettings()); + + // ... And I ask for a subset with valid arguments + // Then: + // ... It should throw an exception + Assert.Throws(() => q.GetSubset(0, 0, 0, 2)); + } + + [Theory] + [InlineData(-1)] // Invalid batch, too low + [InlineData(2)] // Invalid batch, too high + public void QuerySubsetInvalidParamsTest(int batchIndex) { // If I have an executed query Query q = Common.GetBasicExecutedQuery(); @@ -57,7 +75,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... And I ask for a subset with an invalid result set index // Then: // ... It should throw an exception - Assert.Throws(() => q.GetSubset(0, resultSetIndex, rowStartInex, rowCount)); + Assert.Throws(() => q.GetSubset(batchIndex, 0, 0, 1)); } #endregion diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json index 882f0af5..6fe58d61 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json @@ -14,8 +14,7 @@ "moq": "4.6.36-alpha", "Microsoft.SqlTools.ServiceLayer": { "target": "project" - }, - "System.Diagnostics.TraceSource": "4.0.0" + } }, "testRunner": "xunit", "frameworks": { From 91ed9aea59b67efa65b97c9a9bd45c499282f187 Mon Sep 17 00:00:00 2001 From: benrr101 Date: Mon, 22 Aug 2016 12:04:43 -0700 Subject: [PATCH 6/9] Cleanup for comments/copyright --- .../QueryExecution/Batch.cs | 22 ++++++++++++++++--- .../SqlContext/QueryExecutionSettings.cs | 17 ++++++++++---- .../QueryExecution/Common.cs | 7 +++++- .../QueryExecution/DisposeTests.cs | 7 +++++- .../QueryExecution/ExecuteTests.cs | 7 +++++- .../QueryExecution/SubsetTests.cs | 7 +++++- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index 5fb927cd..bd29f1d6 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -1,4 +1,9 @@ -using System; +// +// 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; @@ -10,6 +15,9 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { + /// + /// This class represents a batch within a query + /// public class Batch { private const string RowsAffectedFormat = "({0} row(s) affected)"; @@ -21,10 +29,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public string BatchText { get; set; } /// - /// Whether or not the query has an error + /// 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; } /// @@ -33,7 +44,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public List ResultMessages { get; set; } /// - /// The result sets of the query execution + /// The result sets of the batch execution /// public List ResultSets { get; set; } @@ -70,6 +81,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution ResultMessages = new List(); } + /// + /// 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 diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs index 7e8853a7..4934a4da 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlContext/QueryExecutionSettings.cs @@ -1,16 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.SqlTools.ServiceLayer.SqlContext { + /// + /// Collection of settings related to the execution of queries + /// public class QueryExecutionSettings { + /// + /// Default value for batch separator (de facto standard as per SSMS) + /// private const string DefaultBatchSeparator = "GO"; private string batchSeparator; + /// + /// The configured batch separator, will use a default if a value was not configured + /// public string BatchSeparator { get { return batchSeparator ?? DefaultBatchSeparator; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index 0f2b2907..aa72cb4e 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -1,4 +1,9 @@ -using System; +// +// 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; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs index c0fed697..8ff0affd 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs @@ -1,4 +1,9 @@ -using System; +// +// 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.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index 4f4ec505..f4788014 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,4 +1,9 @@ -using System; +// +// 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.Common; using System.Linq; using System.Threading; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index 6549cf8b..cf0d66a7 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -1,4 +1,9 @@ -using System; +// +// 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.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution; From 51defc1032c88421376274d660086da8dca82be8 Mon Sep 17 00:00:00 2001 From: benrr101 Date: Mon, 22 Aug 2016 13:04:12 -0700 Subject: [PATCH 7/9] Adding correct line numbers for errors --- .../QueryExecution/Batch.cs | 11 +++++++++-- .../QueryExecution/Query.cs | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index bd29f1d6..4b81dfc4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -64,9 +64,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// The 0-indexed line number that this batch started on + /// + private int StartLine { get; set; } + #endregion - public Batch(string batchText) + public Batch(string batchText, int startLine) { // Sanity check for input if (string.IsNullOrEmpty(batchText)) @@ -76,6 +81,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Initialize the internal state BatchText = batchText; + StartLine = startLine - 1; HasExecuted = false; ResultSets = new List(); ResultMessages = new List(); @@ -222,8 +228,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution SqlError sqlError = error as SqlError; if (sqlError != null) { + int lineNumber = sqlError.LineNumber + StartLine; string message = String.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}", - sqlError.Number, sqlError.Class, sqlError.State, sqlError.LineNumber, + sqlError.Number, sqlError.Class, sqlError.State, lineNumber, Environment.NewLine, sqlError.Message); ResultMessages.Add(message); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 66243763..add05b34 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -117,7 +117,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { BatchSeparator = settings.BatchSeparator }); - Batches = parseResult.Script.Batches.Select(b => new Batch(b.Sql)).ToArray(); + Batches = parseResult.Script.Batches.Select(b => new Batch(b.Sql, b.StartLocation.LineNumber)).ToArray(); } /// From 48c7bbaa9f1d2d6446c977e9e1e119dcc60a967a Mon Sep 17 00:00:00 2001 From: benrr101 Date: Mon, 22 Aug 2016 13:41:41 -0700 Subject: [PATCH 8/9] Changing SqlParser version to a version with internals visible --- src/Microsoft.SqlTools.ServiceLayer/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/project.json b/src/Microsoft.SqlTools.ServiceLayer/project.json index 0cfc0788..1636feae 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/project.json +++ b/src/Microsoft.SqlTools.ServiceLayer/project.json @@ -7,7 +7,7 @@ }, "dependencies": { "Newtonsoft.Json": "9.0.1", - "Microsoft.SqlServer.SqlParser": "140.1.4", + "Microsoft.SqlServer.SqlParser": "140.1.5", "System.Data.Common": "4.1.0", "System.Data.SqlClient": "4.1.0" }, From 0371e170288bf587175d6216340792938aa4b955 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 24 Aug 2016 15:25:13 -0700 Subject: [PATCH 9/9] Changes to fix code review comments and bug with handling empty batches --- .../QueryExecution/Batch.cs | 44 +++++++--- .../QueryExecution/Query.cs | 20 ++++- .../QueryExecution/Common.cs | 6 +- .../QueryExecution/ExecuteTests.cs | 87 +++++++++++++++---- .../QueryExecution/SubsetTests.cs | 2 +- 5 files changed, 124 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index 4b81dfc4..38528b60 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -38,15 +38,31 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public bool HasExecuted { get; set; } + /// + /// Internal representation of the messages so we can modify internally + /// + private List resultMessages; + /// /// Messages that have come back from the server /// - public List ResultMessages { get; set; } + public IEnumerable ResultMessages + { + get { return resultMessages; } + } + + /// + /// Internal representation of the result sets so we can modify internally + /// + private List resultSets; /// /// The result sets of the batch execution /// - public List ResultSets { get; set; } + public IEnumerable ResultSets + { + get { return resultSets; } + } /// /// Property for generating a set result set summaries from the result sets @@ -67,7 +83,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// The 0-indexed line number that this batch started on /// - private int StartLine { get; set; } + internal int StartLine { get; set; } #endregion @@ -81,10 +97,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Initialize the internal state BatchText = batchText; - StartLine = startLine - 1; + StartLine = startLine - 1; // -1 to make sure that the line number of the batch is 0-indexed, since SqlParser gives 1-indexed line numbers HasExecuted = false; - ResultSets = new List(); - ResultMessages = new List(); + resultSets = new List(); + resultMessages = new List(); } /// @@ -125,7 +141,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution if (!reader.HasRows && reader.FieldCount == 0) { // Create a message with the number of affected rows -- IF the query affects rows - ResultMessages.Add(reader.RecordsAffected >= 0 + resultMessages.Add(reader.RecordsAffected >= 0 ? string.Format(RowsAffectedFormat, reader.RecordsAffected) : "Command(s) completed successfully."); continue; @@ -145,10 +161,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } // Add the result set to the results of the query - ResultSets.Add(resultSet); + resultSets.Add(resultSet); // Add a message for the number of rows the query returned - ResultMessages.Add(string.Format(RowsAffectedFormat, resultSet.Rows.Count)); + resultMessages.Add(string.Format(RowsAffectedFormat, resultSet.Rows.Count)); } while (await reader.NextResultAsync(cancellationToken)); } } @@ -187,14 +203,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount) { // Sanity check to make sure we have valid numbers - if (resultSetIndex < 0 || resultSetIndex >= ResultSets.Count) + if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count) { throw new ArgumentOutOfRangeException(nameof(resultSetIndex), "Result set index cannot be less than 0" + "or greater than the number of result sets"); } // Retrieve the result set - return ResultSets[resultSetIndex].GetSubset(startRow, rowCount); + return resultSets[resultSetIndex].GetSubset(startRow, rowCount); } #region Private Helpers @@ -208,7 +224,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// Arguments from the event private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args) { - ResultMessages.Add(args.Message); + resultMessages.Add(args.Message); } /// @@ -232,13 +248,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution string message = String.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}", sqlError.Number, sqlError.Class, sqlError.State, lineNumber, Environment.NewLine, sqlError.Message); - ResultMessages.Add(message); + resultMessages.Add(message); } } } else { - ResultMessages.Add(dbe.Message); + resultMessages.Add(dbe.Message); } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index add05b34..73d91e5e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -60,6 +60,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// private ConnectionInfo EditorConnection { get; set; } + private bool HasExecuteBeenCalled { get; set; } + /// /// Whether or not the query has completed executed, regardless of success or failure /// @@ -68,9 +70,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public bool HasExecuted { - get { return Batches.All(b => b.HasExecuted); } + get { return Batches.Length == 0 ? HasExecuteBeenCalled : Batches.All(b => b.HasExecuted); } internal set { + HasExecuteBeenCalled = value; foreach (var batch in Batches) { batch.HasExecuted = value; @@ -94,7 +97,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public Query(string queryText, ConnectionInfo connection, QueryExecutionSettings settings) { // Sanity check for input - if (String.IsNullOrEmpty(queryText)) + if (string.IsNullOrEmpty(queryText)) { throw new ArgumentNullException(nameof(queryText), "Query text cannot be null"); } @@ -117,7 +120,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { BatchSeparator = settings.BatchSeparator }); - Batches = parseResult.Script.Batches.Select(b => new Batch(b.Sql, b.StartLocation.LineNumber)).ToArray(); + // NOTE: We only want to process batches that have statements (ie, ignore comments and empty lines) + Batches = parseResult.Script.Batches.Where(b => b.Statements.Count > 0) + .Select(b => new Batch(b.Sql, b.StartLocation.LineNumber)).ToArray(); } /// @@ -125,6 +130,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// public async Task Execute() { + // Mark that we've internally executed + HasExecuteBeenCalled = true; + + // Don't actually execute if there aren't any batches to execute + if (Batches.Length == 0) + { + return; + } + // Open up a connection for querying the database string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index aa72cb4e..6d265de2 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -26,6 +26,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public const string StandardQuery = "SELECT * FROM sys.objects"; + public const string InvalidQuery = "SELECT *** FROM sys.objects"; + + public const string NoOpQuery = "-- No ops here, just us chickens."; + public const string OwnerUri = "testFile"; public const int StandardRows = 5; @@ -55,7 +59,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public static Batch GetBasicExecutedBatch() { - Batch batch = new Batch(StandardQuery); + Batch batch = new Batch(StandardQuery, 1); batch.Execute(CreateTestConnection(new[] {StandardTestData}, false), CancellationToken.None).Wait(); return batch; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index f4788014..ee98f8a2 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -28,7 +28,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public void BatchCreationTest() { // If I create a new batch... - Batch batch = new Batch(Common.StandardQuery); + Batch batch = new Batch(Common.StandardQuery, 1); // Then: // ... The text of the batch should be stored @@ -42,13 +42,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Empty(batch.ResultSets); Assert.Empty(batch.ResultSummaries); Assert.Empty(batch.ResultMessages); + + // ... The start line of the batch should be 0 + Assert.Equal(0, batch.StartLine); } [Fact] public void BatchExecuteNoResultSets() { // If I execute a query that should get no result sets - Batch batch = new Batch(Common.StandardQuery); + Batch batch = new Batch(Common.StandardQuery, 1); batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None).Wait(); // Then: @@ -65,7 +68,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.NotNull(batch.ResultSummaries); // ... There should be a message for how many rows were affected - Assert.Equal(1, batch.ResultMessages.Count); + Assert.Equal(1, batch.ResultMessages.Count()); } [Fact] @@ -75,7 +78,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); // If I execute a query that should get one result set - Batch batch = new Batch(Common.StandardQuery); + Batch batch = new Batch(Common.StandardQuery, 1); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -84,20 +87,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.False(batch.HasError, "The batch should not have an error"); // ... There should be exactly one result set - Assert.Equal(resultSets, batch.ResultSets.Count); + Assert.Equal(resultSets, batch.ResultSets.Count()); Assert.Equal(resultSets, batch.ResultSummaries.Length); // ... Inside the result set should be with 5 rows - Assert.Equal(Common.StandardRows, batch.ResultSets[0].Rows.Count); + Assert.Equal(Common.StandardRows, batch.ResultSets.First().Rows.Count); Assert.Equal(Common.StandardRows, batch.ResultSummaries[0].RowCount); // ... Inside the result set should have 5 columns and 5 column definitions - Assert.Equal(Common.StandardColumns, batch.ResultSets[0].Rows[0].Length); - Assert.Equal(Common.StandardColumns, batch.ResultSets[0].Columns.Length); + Assert.Equal(Common.StandardColumns, batch.ResultSets.First().Rows[0].Length); + Assert.Equal(Common.StandardColumns, batch.ResultSets.First().Columns.Length); Assert.Equal(Common.StandardColumns, batch.ResultSummaries[0].ColumnInfo.Length); // ... There should be a message for how many rows were affected - Assert.Equal(resultSets, batch.ResultMessages.Count); + Assert.Equal(resultSets, batch.ResultMessages.Count()); } [Fact] @@ -108,7 +111,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(dataset, false); // If I execute a query that should get two result sets - Batch batch = new Batch(Common.StandardQuery); + Batch batch = new Batch(Common.StandardQuery, 1); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -117,7 +120,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.False(batch.HasError, "The batch should not have an error"); // ... There should be exactly two result sets - Assert.Equal(resultSets, batch.ResultSets.Count); + Assert.Equal(resultSets, batch.ResultSets.Count()); foreach (ResultSet rs in batch.ResultSets) { @@ -142,7 +145,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution } // ... There should be a message for how many rows were affected - Assert.Equal(resultSets, batch.ResultMessages.Count); + Assert.Equal(resultSets, batch.ResultMessages.Count()); } [Fact] @@ -151,7 +154,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); // If I execute a batch that is invalid - Batch batch = new Batch(Common.StandardQuery); + Batch batch = new Batch(Common.StandardQuery, 1); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -173,7 +176,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false); // If I execute a batch - Batch batch = new Batch(Common.StandardQuery); + Batch batch = new Batch(Common.StandardQuery, 1); batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); // Then: @@ -203,7 +206,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... I create a batch that has an empty query // Then: // ... It should throw an exception - Assert.Throws(() => new Batch(query)); + Assert.Throws(() => new Batch(query, 1)); } #endregion @@ -269,6 +272,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Equal(1, query.BatchSummaries.Length); } + [Fact] + public void QueryExecuteNoOpBatch() + { + // If: + // ... I create a query from a single batch that does nothing + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); + Query query = new Query(Common.NoOpQuery, ci, new QueryExecutionSettings()); + + // Then: + // ... I should get no batches back + Assert.NotEmpty(query.QueryText); + Assert.Empty(query.Batches); + Assert.False(query.HasExecuted); + Assert.Throws(() => query.BatchSummaries); + + // If: + // ... I Then execute the query + query.Execute().Wait(); + + // Then: + // ... The query should have completed successfully with no batch summaries returned + Assert.True(query.HasExecuted); + Assert.Empty(query.BatchSummaries); + } + [Fact] public void QueryExecuteMultipleBatches() { @@ -297,13 +325,40 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Equal(2, query.BatchSummaries.Length); } + [Fact] + public void QueryExecuteMultipleBatchesWithNoOp() + { + // If: + // ... I create a query from a two batches (with separator) + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); + string queryText = string.Format("{0}\r\nGO\r\n{1}", Common.StandardQuery, Common.NoOpQuery); + Query query = new Query(queryText, ci, new QueryExecutionSettings()); + + // Then: + // ... I should get back one batch to execute that hasn't been executed + Assert.NotEmpty(query.QueryText); + Assert.NotEmpty(query.Batches); + Assert.Equal(1, query.Batches.Length); + Assert.False(query.HasExecuted); + Assert.Throws(() => query.BatchSummaries); + + // If: + // .. I then execute the query + query.Execute().Wait(); + + // ... The query should have completed successfully with one batch summary returned + Assert.True(query.HasExecuted); + Assert.NotEmpty(query.BatchSummaries); + Assert.Equal(1, query.BatchSummaries.Length); + } + [Fact] public void QueryExecuteInvalidBatch() { // If: // ... I create a query from an invalid batch ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); - Query query = new Query("SELECT *** FROM sys.objects", ci, new QueryExecutionSettings()); + Query query = new Query(Common.InvalidQuery, ci, new QueryExecutionSettings()); // Then: // ... I should get back a query with one batch not executed diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index cf0d66a7..ad0f7075 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -143,7 +143,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri }; var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); - //queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; + queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; // ... And I then ask for a valid set of results from it var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 };