From f1eebd989a84b6403c8b928e605f5a18d38227d3 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 3 Aug 2016 14:27:09 -0700 Subject: [PATCH 01/12] Adding other global files as solution items --- sqltoolsservice.sln | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sqltoolsservice.sln b/sqltoolsservice.sln index 828baca9..cd55b538 100644 --- a/sqltoolsservice.sln +++ b/sqltoolsservice.sln @@ -8,7 +8,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AB9CA2B8-6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{32DC973E-9EEA-4694-B1C2-B031167AB945}" ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore global.json = global.json + nuget.config = nuget.config + README.md = README.md EndProjectSection EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.SqlTools.ServiceLayer", "src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.xproj", "{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}" From da3d45a3e7e50a36900b857cc32bead7a6d25fe7 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 3 Aug 2016 14:27:53 -0700 Subject: [PATCH 02/12] WIP for query execution --- .../LanguageServices/AutoCompleteService.cs | 23 ++--- .../Contracts/QueryDisposeRequest.cs | 37 ++++++++ .../QueryExecuteCompleteNotification.cs | 25 ++++++ .../Contracts/QueryExecuteRequest.cs | 44 +++++++++ .../Contracts/QueryExecuteResultsRequest.cs | 54 +++++++++++ .../Contracts/ResultSet.cs | 33 +++++++ .../Contracts/ResultSetSubset.cs | 11 +++ .../Contracts/ResultSetSummary.cs | 26 ++++++ .../QueryExecutionServices/Query.cs | 69 ++++++++++++++ .../QueryExecutionService.cs | 89 +++++++++++++++++++ 10 files changed, 397 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs index 9af007a6..715a975d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/AutoCompleteService.cs @@ -54,12 +54,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// public IEnumerable AutoCompleteList { get; private set; } - public void InitializeService(ServiceHost serviceHost) - { - // Register a callback for when a connection is created - ConnectionService.Instance.RegisterOnConnectionTask(UpdateAutoCompleteCache); - } - /// /// Update the cached autocomplete candidate list when the user connects to a database /// TODO: Update with refactoring/async @@ -71,16 +65,17 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices command.CommandText = "SELECT name FROM sys.tables"; command.CommandTimeout = 15; command.CommandType = CommandType.Text; - var reader = await command.ExecuteReaderAsync(); - - List results = new List(); - while (await reader.ReadAsync()) + using (var reader = await command.ExecuteReaderAsync()) { - results.Add(reader[0].ToString()); - } - AutoCompleteList = results; - await Task.FromResult(0); + List results = new List(); + while (await reader.ReadAsync()) + { + results.Add(reader[0].ToString()); + } + + AutoCompleteList = results; + } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs new file mode 100644 index 00000000..e34e7dbc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs @@ -0,0 +1,37 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + /// + /// Parameters for the query dispose request + /// + public class QueryDisposeParams + { + public Guid QueryId { get; set; } + } + + /// + /// Parameters to return as the result of a query dispose request + /// + public class QueryDisposeResult + { + /// + /// Any error messages that occurred during disposing the result set. Optional, can be set + /// to null if there were no errors. + /// + public string Messages { get; set; } + } + + public class QueryDisposeRequest + { + public static readonly + RequestType Type = + RequestType.Create("query/dispose"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs new file mode 100644 index 00000000..22a210c2 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + public class QueryExecuteCompleteNotification + { + /// + /// Any messages that came back from the server during execution of the query + /// + public string[] Messages { get; set; } + + /// + /// Whether or not the query was successful. True indicates errors, false indicates success + /// + public bool Error { get; set; } + + /// + /// Summaries of the result sets that were returned with the query + /// + public ResultSetSummary[] ResultSetSummaries { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs new file mode 100644 index 00000000..95fd1548 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs @@ -0,0 +1,44 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + /// + /// Parameters for the query execute request + /// + public class QueryExecuteParams + { + /// + /// The text of the query to execute + /// + public string QueryText { get; set; } + + /// + /// URI for the editor that is asking for the query execute + /// + public string OwnerUri { get; set; } + } + + /// + /// Parameters for the query execute result + /// + public class QueryExecuteResult + { + /// + /// Connection error messages. Optional, can be set to null to indicate no errors + /// + public string Messages { get; set; } + } + + public class QueryExecuteRequest + { + public static readonly + RequestType Type = + RequestType.Create("query/execute"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs new file mode 100644 index 00000000..7a31dcb4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs @@ -0,0 +1,54 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + /// + /// Parameters for a query result subset retrieval request + /// + public class QueryExecuteSubsetParams + { + /// + /// ID of the query to look up the results for + /// + public Guid QueryId { get; set; } + + /// + /// Index of the result set to get the results from + /// + public int ResultSetIndex { get; set; } + + /// + /// Beginning index of the rows to return from the selected resultset. This index will be + /// included in the results. + /// + public int RowsStartIndex { get; set; } + + /// + /// Number of rows to include in the result of this request. If the number of the rows + /// exceeds the number of rows available after the start index, all available rows after + /// the start index will be returned. + /// + public int RowsCount { get; set; } + } + + /// + /// + /// + public class QueryExecuteSubsetResult + { + + } + + public class QueryExecuteSubsetRequest + { + public static readonly + RequestType Type = + RequestType.Create("query/subset"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs new file mode 100644 index 00000000..c3ec6f3d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + public class ResultSet + { + public DbColumn[] Columns { get; set; } + + public List Rows { get; private set; } + + public ResultSet() + { + Rows = new List(); + } + + /// + /// Add a row of data to the result set using a that has already + /// read in a row. + /// + /// A that has already had a read performed + public void AddRow(DbDataReader reader) + { + List row = new List(); + for (int i = 0; i < reader.FieldCount; ++i) + { + row.Add(reader.GetValue(i)); + } + Rows.Add(row.ToArray()); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs new file mode 100644 index 00000000..092e58b3 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + public class ResultSetSubset + { + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs new file mode 100644 index 00000000..b989c135 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +{ + public class ResultSetSummary + { + /// + /// The ID of the result set within the query results + /// + public int Id { get; set; } + + /// + /// The number of rows that was returned with the resultset + /// + public long RowCount { get; set; } + + /// + /// Details about the columns that are provided as solutions + /// + public DbColumn ColumnInfo { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs new file mode 100644 index 00000000..d2a49bb3 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices +{ + public class Query //: IDisposable + { + public string QueryText { get; set; } + + public DbConnection SqlConnection { get; set; } + + private readonly CancellationTokenSource cancellationSource; + + public List ResultSets { get; set; } + + public Query(string queryText, DbConnection connection) + { + QueryText = queryText; + SqlConnection = connection; + ResultSets = new List(); + cancellationSource = new CancellationTokenSource(); + } + + public async Task Execute() + { + // Open the connection, if it's not already open + if ((SqlConnection.State & ConnectionState.Open) == 0) + { + await SqlConnection.OpenAsync(cancellationSource.Token); + } + + // Create a command that we'll use for executing the query + using (DbCommand command = SqlConnection.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 new result set that we'll use to store all the data + ResultSet resultSet = new ResultSet(); + if (reader.CanGetColumnSchema()) + { + resultSet.Columns = reader.GetColumnSchema().ToArray(); + } + + // Read until we hit the end of the result set + while (await reader.ReadAsync(cancellationSource.Token)) + { + resultSet.AddRow(reader); + } + + // Add the result set to the results of the query + ResultSets.Add(resultSet); + } while (await reader.NextResultAsync(cancellationSource.Token)); + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs new file mode 100644 index 00000000..b3ec4886 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices +{ + public sealed class QueryExecutionService + { + #region Singleton Instance Implementation + + private static readonly Lazy instance = new Lazy(() => new QueryExecutionService()); + + public static QueryExecutionService Instance + { + get { return instance.Value; } + } + + private QueryExecutionService() { } + + #endregion + + #region Properties + + private readonly Lazy> queries = + new Lazy>(() => new ConcurrentDictionary()); + + private ConcurrentDictionary Queries + { + get { return queries.Value; } + } + + #endregion + + #region Public Methods + + /// + /// + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + // Register handlers for requests + serviceHost.SetRequestHandler(QueryExecuteRequest.Type, HandleExecuteRequest); + serviceHost.SetRequestHandler(QueryExecuteSubsetRequest.Type, HandleResultSubsetRequest); + serviceHost.SetRequestHandler(QueryDisposeRequest.Type, HandleDisposeRequest); + + // Register handlers for events + } + + #endregion + + #region Request Handlers + + private async Task HandleExecuteRequest(QueryExecuteParams executeParams, + RequestContext requestContext) + { + + } + + private async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, + RequestContext requestContext) + { + await Task.FromResult(0); + } + + private async Task HandleDisposeRequest(QueryDisposeParams disposeParams, + RequestContext requestContext) + { + string messages = null; + + Query result; + if (!Queries.TryRemove(disposeParams., out result)) + { + messages = "Failed to dispose query, ID not found."; + } + + await requestContext.SendResult(new QueryDisposeResult + { + Messages = messages + }); + } + + #endregion + + } +} From 3ba22c94ac38c7bdea8da35378b547b0345167b5 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 4 Aug 2016 14:48:58 -0700 Subject: [PATCH 03/12] WIP for QueryExecution, mostly complete --- .../Contracts/QueryDisposeRequest.cs | 4 +- .../QueryExecuteCompleteNotification.cs | 21 ++- .../Contracts/QueryExecuteRequest.cs | 2 +- .../Contracts/QueryExecuteSubsetRequest.cs} | 9 +- .../Contracts/ResultSetSubset.cs | 13 ++ .../Contracts/ResultSetSummary.cs | 12 +- .../QueryExecution/Query.cs | 144 +++++++++++++++++ .../QueryExecution/QueryExecutionService.cs | 153 ++++++++++++++++++ .../Contracts => QueryExecution}/ResultSet.cs | 2 +- .../Contracts/ResultSetSubset.cs | 11 -- .../QueryExecutionServices/Query.cs | 69 -------- .../QueryExecutionService.cs | 89 ---------- 12 files changed, 338 insertions(+), 191 deletions(-) rename src/Microsoft.SqlTools.ServiceLayer/{QueryExecutionServices => QueryExecution}/Contracts/QueryDisposeRequest.cs (89%) rename src/Microsoft.SqlTools.ServiceLayer/{QueryExecutionServices => QueryExecution}/Contracts/QueryExecuteCompleteNotification.cs (51%) rename src/Microsoft.SqlTools.ServiceLayer/{QueryExecutionServices => QueryExecution}/Contracts/QueryExecuteRequest.cs (94%) rename src/Microsoft.SqlTools.ServiceLayer/{QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs => QueryExecution/Contracts/QueryExecuteSubsetRequest.cs} (85%) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs rename src/Microsoft.SqlTools.ServiceLayer/{QueryExecutionServices => QueryExecution}/Contracts/ResultSetSummary.cs (59%) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs rename src/Microsoft.SqlTools.ServiceLayer/{QueryExecutionServices/Contracts => QueryExecution}/ResultSet.cs (92%) delete mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs delete mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs delete mode 100644 src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs similarity index 89% rename from src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs rename to src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs index e34e7dbc..51e1b5dd 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryDisposeRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs @@ -6,14 +6,14 @@ using System; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { /// /// Parameters for the query dispose request /// public class QueryDisposeParams { - public Guid QueryId { get; set; } + public string OwnerUri { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs similarity index 51% rename from src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs rename to src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs index 22a210c2..b5c69941 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteCompleteNotification.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs @@ -1,12 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { - public class QueryExecuteCompleteNotification + public class QueryExecuteCompleteParams { + /// + /// URI for the editor that owns the query + /// + public string OwnerUri { get; set; } + /// /// Any messages that came back from the server during execution of the query /// @@ -22,4 +24,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts /// public ResultSetSummary[] ResultSetSummaries { get; set; } } + + public class QueryExecuteCompleteEvent + { + public static readonly + EventType Type = + EventType.Create("query/complete"); + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs similarity index 94% rename from src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs rename to src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs index 95fd1548..59453fb9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs @@ -6,7 +6,7 @@ using System; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { /// /// Parameters for the query execute request diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs similarity index 85% rename from src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs rename to src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs index 7a31dcb4..7cd607a6 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/QueryExecuteResultsRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs @@ -6,7 +6,7 @@ using System; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { /// /// Parameters for a query result subset retrieval request @@ -16,7 +16,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts /// /// ID of the query to look up the results for /// - public Guid QueryId { get; set; } + public string OwnerId { get; set; } /// /// Index of the result set to get the results from @@ -38,11 +38,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts } /// - /// + /// Parameters for the result of a subset retrieval request /// public class QueryExecuteSubsetResult { - + public string Message { get; set; } + public ResultSetSubset ResultSubset { get; set; } } public class QueryExecuteSubsetRequest diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs new file mode 100644 index 00000000..a9256581 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +{ + public class ResultSetSubset + { + public int RowCount { get; set; } + public object[][] Rows { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs similarity index 59% rename from src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs rename to src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs index b989c135..416aafb8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSummary.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; +using System.Data.Common; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { public class ResultSetSummary { @@ -16,11 +12,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts /// /// The number of rows that was returned with the resultset /// - public long RowCount { get; set; } + public int RowCount { get; set; } /// /// Details about the columns that are provided as solutions /// - public DbColumn ColumnInfo { get; set; } + public DbColumn[] ColumnInfo { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs new file mode 100644 index 00000000..e146270f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +{ + public class Query //: IDisposable + { + #region Properties + + public string QueryText { get; set; } + + public ConnectionInfo EditorConnection { get; set; } + + private readonly CancellationTokenSource cancellationSource; + + public List ResultSets { get; set; } + + public ResultSetSummary[] ResultSummary + { + get + { + return ResultSets.Select((set, index) => new ResultSetSummary + { + ColumnInfo = set.Columns, + Id = index, + RowCount = set.Rows.Count + }).ToArray(); + } + } + + public bool HasExecuted { get; set; } + + #endregion + + public Query(string queryText, ConnectionInfo connection) + { + // Sanity check for input + if (queryText == null) + { + throw new ArgumentNullException(nameof(queryText), "Query text cannot be null"); + } + if (connection == null) + { + throw new ArgumentNullException(nameof(connection), "Connection cannot be null"); + } + + // Initialize the internal state + QueryText = queryText; + EditorConnection = connection; + HasExecuted = false; + ResultSets = new List(); + cancellationSource = new CancellationTokenSource(); + } + + 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."); + } + + // Create a connection from the connection details + using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(EditorConnection.ConnectionDetails)) + { + 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 new result set that we'll use to store all the data + ResultSet resultSet = new ResultSet(); + if (reader.CanGetColumnSchema()) + { + resultSet.Columns = reader.GetColumnSchema().ToArray(); + } + + // Read until we hit the end of the result set + while (await reader.ReadAsync(cancellationSource.Token)) + { + resultSet.AddRow(reader); + } + + // Add the result set to the results of the query + ResultSets.Add(resultSet); + } while (await reader.NextResultAsync(cancellationSource.Token)); + } + } + } + + // Mark that we have executed + HasExecuted = true; + } + + public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount) + { + // Sanity check that the results are available + if (!HasExecuted) + { + throw new InvalidOperationException("The query has not completed, yet."); + } + + // 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"); + } + 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 + }; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs new file mode 100644 index 00000000..d8ff0811 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution +{ + public sealed class QueryExecutionService + { + #region Singleton Instance Implementation + + private static readonly Lazy instance = new Lazy(() => new QueryExecutionService()); + + public static QueryExecutionService Instance + { + get { return instance.Value; } + } + + private QueryExecutionService() { } + + #endregion + + #region Properties + + private readonly Lazy> queries = + new Lazy>(() => new ConcurrentDictionary()); + + private ConcurrentDictionary ActiveQueries + { + get { return queries.Value; } + } + + #endregion + + #region Public Methods + + /// + /// + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + // Register handlers for requests + serviceHost.SetRequestHandler(QueryExecuteRequest.Type, HandleExecuteRequest); + serviceHost.SetRequestHandler(QueryExecuteSubsetRequest.Type, HandleResultSubsetRequest); + serviceHost.SetRequestHandler(QueryDisposeRequest.Type, HandleDisposeRequest); + + // Register handlers for events + } + + #endregion + + #region Request Handlers + + private async Task HandleExecuteRequest(QueryExecuteParams executeParams, + RequestContext requestContext) + { + // Attempt to get the connection for the editor + ConnectionInfo connectionInfo; + if(!ConnectionService.Instance.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + { + await requestContext.SendError("This editor is not connected to a database."); + return; + } + + // If there is already an in-flight query, error out + Query newQuery = new Query(executeParams.QueryText, connectionInfo); + if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) + { + await requestContext.SendError("A query is already in progress for this editor session." + + "Please cancel this query or wait for its completion."); + return; + } + + // Launch the query and respond with successfully launching it + Task executeTask = newQuery.Execute(); + await requestContext.SendResult(new QueryExecuteResult + { + Messages = null + }); + + // Wait for query execution and then send back the results + await Task.WhenAll(executeTask); + QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams + { + Error = false, + Messages = new string[]{}, // TODO: Figure out how to get messages back from the server + OwnerUri = executeParams.OwnerUri, + ResultSetSummaries = newQuery.ResultSummary + }; + await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); + } + + private async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, + RequestContext requestContext) + { + // Attempt to load the query + Query query; + if (!ActiveQueries.TryGetValue(subsetParams.OwnerId, out query)) + { + var errorResult = new QueryExecuteSubsetResult + { + Message = "The requested query does not exist." + }; + await requestContext.SendResult(errorResult); + return; + } + + try + { + // Retrieve the requested subset and return it + var result = new QueryExecuteSubsetResult + { + Message = null, + ResultSubset = query.GetSubset( + subsetParams.ResultSetIndex, subsetParams.RowsStartIndex, subsetParams.RowsCount) + }; + await requestContext.SendResult(result); + } + catch (Exception e) + { + await requestContext.SendResult(new QueryExecuteSubsetResult + { + Message = e.Message + }); + } + } + + private async Task HandleDisposeRequest(QueryDisposeParams disposeParams, + RequestContext requestContext) + { + // Attempt to remove the query for the owner uri + Query result; + if (!ActiveQueries.TryRemove(disposeParams.OwnerUri, out result)) + { + await requestContext.SendError("Failed to dispose query, ID not found."); + return; + } + + // Success + await requestContext.SendResult(new QueryDisposeResult + { + Messages = null + }); + } + + #endregion + + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs similarity index 92% rename from src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs rename to src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs index c3ec6f3d..5de88521 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSet.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Data.Common; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { public class ResultSet { diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs deleted file mode 100644 index 092e58b3..00000000 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Contracts/ResultSetSubset.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts -{ - public class ResultSetSubset - { - } -} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs deleted file mode 100644 index d2a49bb3..00000000 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/Query.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts; - -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices -{ - public class Query //: IDisposable - { - public string QueryText { get; set; } - - public DbConnection SqlConnection { get; set; } - - private readonly CancellationTokenSource cancellationSource; - - public List ResultSets { get; set; } - - public Query(string queryText, DbConnection connection) - { - QueryText = queryText; - SqlConnection = connection; - ResultSets = new List(); - cancellationSource = new CancellationTokenSource(); - } - - public async Task Execute() - { - // Open the connection, if it's not already open - if ((SqlConnection.State & ConnectionState.Open) == 0) - { - await SqlConnection.OpenAsync(cancellationSource.Token); - } - - // Create a command that we'll use for executing the query - using (DbCommand command = SqlConnection.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 new result set that we'll use to store all the data - ResultSet resultSet = new ResultSet(); - if (reader.CanGetColumnSchema()) - { - resultSet.Columns = reader.GetColumnSchema().ToArray(); - } - - // Read until we hit the end of the result set - while (await reader.ReadAsync(cancellationSource.Token)) - { - resultSet.AddRow(reader); - } - - // Add the result set to the results of the query - ResultSets.Add(resultSet); - } while (await reader.NextResultAsync(cancellationSource.Token)); - } - } - } - } -} diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs deleted file mode 100644 index b3ec4886..00000000 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecutionServices/QueryExecutionService.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Microsoft.SqlTools.ServiceLayer.Hosting; -using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; -using Microsoft.SqlTools.ServiceLayer.QueryExecutionServices.Contracts; - -namespace Microsoft.SqlTools.ServiceLayer.QueryExecutionServices -{ - public sealed class QueryExecutionService - { - #region Singleton Instance Implementation - - private static readonly Lazy instance = new Lazy(() => new QueryExecutionService()); - - public static QueryExecutionService Instance - { - get { return instance.Value; } - } - - private QueryExecutionService() { } - - #endregion - - #region Properties - - private readonly Lazy> queries = - new Lazy>(() => new ConcurrentDictionary()); - - private ConcurrentDictionary Queries - { - get { return queries.Value; } - } - - #endregion - - #region Public Methods - - /// - /// - /// - /// - public void InitializeService(ServiceHost serviceHost) - { - // Register handlers for requests - serviceHost.SetRequestHandler(QueryExecuteRequest.Type, HandleExecuteRequest); - serviceHost.SetRequestHandler(QueryExecuteSubsetRequest.Type, HandleResultSubsetRequest); - serviceHost.SetRequestHandler(QueryDisposeRequest.Type, HandleDisposeRequest); - - // Register handlers for events - } - - #endregion - - #region Request Handlers - - private async Task HandleExecuteRequest(QueryExecuteParams executeParams, - RequestContext requestContext) - { - - } - - private async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, - RequestContext requestContext) - { - await Task.FromResult(0); - } - - private async Task HandleDisposeRequest(QueryDisposeParams disposeParams, - RequestContext requestContext) - { - string messages = null; - - Query result; - if (!Queries.TryRemove(disposeParams., out result)) - { - messages = "Failed to dispose query, ID not found."; - } - - await requestContext.SendResult(new QueryDisposeResult - { - Messages = messages - }); - } - - #endregion - - } -} From 05e4c4f3a9fc5e6502979948f4e32fb9d926c91a Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 4 Aug 2016 17:20:52 -0700 Subject: [PATCH 04/12] Final changes before V1 testing --- src/Microsoft.SqlTools.ServiceLayer/Program.cs | 5 +++++ src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/Program.cs b/src/Microsoft.SqlTools.ServiceLayer/Program.cs index f6054354..c0f547c2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Program.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Program.cs @@ -2,12 +2,16 @@ // 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.EditorServices.Utility; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer { @@ -46,6 +50,7 @@ namespace Microsoft.SqlTools.ServiceLayer AutoCompleteService.Instance.InitializeService(serviceHost); LanguageService.Instance.InitializeService(serviceHost, sqlToolsContext); ConnectionService.Instance.InitializeService(serviceHost); + QueryExecutionService.Instance.InitializeService(serviceHost); serviceHost.Initialize(); serviceHost.WaitForExit(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index e146270f..3e189ece 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -68,7 +68,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } // Create a connection from the connection details - using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(EditorConnection.ConnectionDetails)) + string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); + using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) { await conn.OpenAsync(cancellationSource.Token); From 0740e81dab43a458a0a09e07713857e1dd6fa73a Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 5 Aug 2016 11:11:50 -0700 Subject: [PATCH 05/12] Stub files for tests for query execution --- .../QueryExecution/ResultSet.cs | 2 +- .../QueryExecution/DisposeTests.cs | 11 ++++ .../QueryExecution/ExecuteTests.cs | 54 +++++++++++++++++++ .../QueryExecution/SubsetTests.cs | 11 ++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs index 5de88521..ee7d7852 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Data.Common; -namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts +namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { public class ResultSet { diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs new file mode 100644 index 00000000..def3c6b6 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class DisposeTests + { + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs new file mode 100644 index 00000000..4e2ccab1 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.Test.Utility; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class ExecuteTests + { + [Fact] + public void QueryCreationTest() + { + // If I create a new query... + Query query = new Query("NO OP", CreateTestConnectionInfo()); + + // Then: + // ... It should not have executed + Assert.False(query.HasExecuted, "The query should not have executed."); + + // ... The results should be empty + Assert.Empty(query.ResultSets); + Assert.Empty(query.ResultSummary); + } + + private static ConnectionInfo CreateTestConnectionInfo() + { + // Create connection info + ConnectionDetails connDetails = new ConnectionDetails + { + UserName = "sa", + Password = "Yukon900", + DatabaseName = "AdventureWorks2016CTP3_2", + ServerName = "sqltools11" + }; + +#if !USE_LIVE_CONNECTION + // Use the mock db connection factory + ISqlConnectionFactory factory = new TestSqlConnectionFactory(); +#else + // Use a real db connection factory + ISqlConnectionFactory factory = new SqlConnectionFactory(); +#endif + + return new ConnectionInfo(factory, "test://test", connDetails); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs new file mode 100644 index 00000000..f64dd96c --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class SubsetTests + { + } +} From 368a98c8e0f7d3501929a3de33d4ddddc5acf74d Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 5 Aug 2016 14:55:59 -0700 Subject: [PATCH 06/12] Quick fix for @anthonydresser --- src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs index 560805d7..3099a3d5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs @@ -124,6 +124,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace // type SqlTools have a path starting with 'untitled'. return filePath.StartsWith("inmemory") || + filePath.StartsWith("tsqloutput") || filePath.StartsWith("untitled"); } From a06003c966fdd1c91e198304a9e8d29877668ec9 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 5 Aug 2016 15:08:28 -0700 Subject: [PATCH 07/12] Another change for @anthonydresser --- .../QueryExecution/Contracts/QueryExecuteSubsetRequest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs index 7cd607a6..8a7b3587 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs @@ -14,9 +14,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts public class QueryExecuteSubsetParams { /// - /// ID of the query to look up the results for + /// URI for the file that owns the query to look up the results for /// - public string OwnerId { get; set; } + public string OwnerUri { get; set; } /// /// Index of the result set to get the results from From a5582889bfa572c60381ad5e544279ee8814c7ae Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 5 Aug 2016 15:11:58 -0700 Subject: [PATCH 08/12] Forgot to make corresponding changes in other files --- .../QueryExecution/QueryExecutionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index d8ff0811..e9f0ba1a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -99,7 +99,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { // Attempt to load the query Query query; - if (!ActiveQueries.TryGetValue(subsetParams.OwnerId, out query)) + if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) { var errorResult = new QueryExecuteSubsetResult { From 9f371cd0bca292e5796f0188bb371d8d9eeb4577 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 5 Aug 2016 18:38:21 -0700 Subject: [PATCH 09/12] Unit tests, part 1 --- .../QueryExecution/Query.cs | 14 +- .../QueryExecution/ExecuteTests.cs | 201 ++++++++++++++-- .../Utility/TestDbDataReader.cs | 215 ++++++++++++++++++ .../Utility/TestObjects.cs | 189 ++------------- .../project.json | 3 +- 5 files changed, 431 insertions(+), 191 deletions(-) create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 3e189ece..831e981b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -84,19 +84,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { do { - // Create a new result set that we'll use to store all the data - ResultSet resultSet = new ResultSet(); - if (reader.CanGetColumnSchema()) + // TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows + if (!reader.HasRows) { - resultSet.Columns = reader.GetColumnSchema().ToArray(); + 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)); diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index 4e2ccab1..3eb923c5 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,24 +1,35 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.Common; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; -using Microsoft.SqlTools.Test.Utility; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Moq; +using Moq.Protected; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class ExecuteTests { + private static Dictionary[] testData = + { + 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"} }, + }; + [Fact] public void QueryCreationTest() { // If I create a new query... - Query query = new Query("NO OP", CreateTestConnectionInfo()); + Query query = new Query("NO OP", CreateTestConnectionInfo(null)); // Then: // ... It should not have executed @@ -29,7 +40,173 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Empty(query.ResultSummary); } - private static ConnectionInfo CreateTestConnectionInfo() + [Fact] + public void QueryExecuteNoResultSets() + { + // If I execute a query that should get no result sets + Query query = new Query("Query with no result sets", CreateTestConnectionInfo(null)); + query.Execute().Wait(); + + // Then: + // ... It should have executed + Assert.True(query.HasExecuted, "The query should have been marked executed."); + + // ... The results should be empty + Assert.Empty(query.ResultSets); + Assert.Empty(query.ResultSummary); + + // ... The results should not be null + Assert.NotNull(query.ResultSets); + Assert.NotNull(query.ResultSummary); + } + + [Fact] + public void QueryExecuteQueryOneResultSet() + { + ConnectionInfo ci = CreateTestConnectionInfo(new[] {testData}); + + // 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(); + + // Then: + // ... It should have executed + Assert.True(query.HasExecuted, "The query should have been marked executed."); + + // ... There should be exactly one result set + Assert.Equal(resultSets, query.ResultSets.Count); + + // ... Inside the result set should be with 5 rows + Assert.Equal(rows, query.ResultSets[0].Rows.Count); + + // ... 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); + } + + [Fact] + public void QueryExecuteQueryTwoResultSets() + { + var dataset = new[] {testData, testData}; + int resultSets = dataset.Length; + int rows = testData.Length; + int columns = testData[0].Count; + ConnectionInfo ci = CreateTestConnectionInfo(dataset); + + // If I execute a query that should get two result sets + Query query = new Query("Query with two result sets", ci); + query.Execute().Wait(); + + // Then: + // ... It should have executed + Assert.True(query.HasExecuted, "The query should have been marked executed."); + + // ... There should be exactly two result sets + Assert.Equal(resultSets, query.ResultSets.Count); + + foreach (ResultSet rs in query.ResultSets) + { + // ... Each result set should have 5 rows + Assert.Equal(rows, 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); + } + + // ... There should be exactly two result set summaries + Assert.Equal(resultSets, query.ResultSummary.Length); + + foreach (ResultSetSummary rs in query.ResultSummary) + { + // ... 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); + } + } + + #region Mocking + + //private static DbDataReader CreateTestReader(int columnCount, int rowCount) + //{ + // var readerMock = new Mock { CallBase = true }; + + // // Setup for column reads + // // TODO: We can't test columns because of oddities with how datatable/GetColumn + + // // Setup for row reads + // var readSequence = readerMock.SetupSequence(dbReader => dbReader.Read()); + // for (int i = 0; i < rowCount; i++) + // { + // readSequence.Returns(true); + // } + // readSequence.Returns(false); + + // // Make sure that if we call for data from the reader it works + // readerMock.Setup(dbReader => dbReader[InColumnRange(columnCount)]) + // .Returns(i => i.ToString()); + // readerMock.Setup(dbReader => dbReader[NotInColumnRange(columnCount)]) + // .Throws(new ArgumentOutOfRangeException()); + // readerMock.Setup(dbReader => dbReader.HasRows) + // .Returns(rowCount > 0); + + // return readerMock.Object; + //} + + //private static int InColumnRange(int columnCount) + //{ + // return Match.Create(i => i < columnCount && i > 0); + //} + + //private static int NotInColumnRange(int columnCount) + //{ + // return Match.Create(i => i >= columnCount || i < 0); + //} + + private static DbCommand CreateTestCommand(Dictionary[][] data) + { + var commandMock = new Mock {CallBase = true}; + commandMock.Protected() + .Setup("ExecuteDbDataReader", It.IsAny()) + .Returns(new TestDbDataReader(data)); + + return commandMock.Object; + } + + private static DbConnection CreateTestConnection(Dictionary[][] data) + { + var connectionMock = new Mock {CallBase = true}; + connectionMock.Protected() + .Setup("CreateDbCommand") + .Returns(CreateTestCommand(data)); + + return connectionMock.Object; + } + + private static ISqlConnectionFactory CreateMockFactory(Dictionary[][] data) + { + var mockFactory = new Mock(); + mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny())) + .Returns(CreateTestConnection(data)); + + return mockFactory.Object; + } + + private static ConnectionInfo CreateTestConnectionInfo(Dictionary[][] data) { // Create connection info ConnectionDetails connDetails = new ConnectionDetails @@ -40,15 +217,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution ServerName = "sqltools11" }; -#if !USE_LIVE_CONNECTION - // Use the mock db connection factory - ISqlConnectionFactory factory = new TestSqlConnectionFactory(); -#else - // Use a real db connection factory - ISqlConnectionFactory factory = new SqlConnectionFactory(); -#endif - - return new ConnectionInfo(factory, "test://test", connDetails); + return new ConnectionInfo(CreateMockFactory(data), "test://test", connDetails); } + + #endregion } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs new file mode 100644 index 00000000..0031ad4a --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs @@ -0,0 +1,215 @@ +// +// 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; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data.Common; +using System.Linq; +using Moq; + +namespace Microsoft.SqlTools.ServiceLayer.Test.Utility +{ + public class TestDbDataReader : DbDataReader, IDbColumnSchemaGenerator + { + + #region Test Specific Implementations + + private Dictionary[][] Data { get; set; } + + public IEnumerator[]> ResultSet { get; private set; } + + private IEnumerator> Rows { get; set; } + + private const string tableNameTestCommand = "SELECT name FROM sys.tables"; + + private List> tableNamesTest = new List> + { + new Dictionary { {"name", "table1"} }, + new Dictionary { {"name", "table2"} } + }; + + public TestDbDataReader(Dictionary[][] data) + { + Data = data; + if (Data != null) + { + ResultSet = ((IEnumerable[]>) Data).GetEnumerator(); + ResultSet.MoveNext(); + } + } + + #endregion + + public override bool HasRows + { + get { return ResultSet != null && ResultSet.Current.Length > 0; } + } + + public override bool Read() + { + if (Rows == null) + { + Rows = ((IEnumerable>) ResultSet.Current).GetEnumerator(); + } + return Rows.MoveNext(); + } + + public override bool NextResult() + { + if (Data == null || !ResultSet.MoveNext()) + { + return false; + } + Rows = ((IEnumerable>)ResultSet.Current).GetEnumerator(); + return true; + } + + public override object GetValue(int ordinal) + { + return this[ordinal]; + } + + public override object this[string name] + { + get { return Rows.Current[name]; } + } + + public override object this[int ordinal] + { + get { return Rows.Current[Rows.Current.Keys.AsEnumerable().ToArray()[ordinal]]; } + } + + public ReadOnlyCollection GetColumnSchema() + { + if (ResultSet?.Current == null || ResultSet.Current.Length <= 0) + { + return new ReadOnlyCollection(new List()); + } + + List columns = new List(); + for (int i = 0; i < ResultSet.Current[0].Count; i++) + { + columns.Add(new Mock().Object); + } + return new ReadOnlyCollection(columns); + } + + public override int FieldCount { get { return Rows?.Current.Count ?? 0; } } + + #region Not Implemented + + public override bool GetBoolean(int ordinal) + { + throw new NotImplementedException(); + } + + public override byte GetByte(int ordinal) + { + throw new NotImplementedException(); + } + + public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) + { + throw new NotImplementedException(); + } + + public override char GetChar(int ordinal) + { + throw new NotImplementedException(); + } + + public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) + { + throw new NotImplementedException(); + } + + public override string GetDataTypeName(int ordinal) + { + throw new NotImplementedException(); + } + + public override DateTime GetDateTime(int ordinal) + { + throw new NotImplementedException(); + } + + public override decimal GetDecimal(int ordinal) + { + throw new NotImplementedException(); + } + + public override double GetDouble(int ordinal) + { + throw new NotImplementedException(); + } + + public override int GetOrdinal(string name) + { + throw new NotImplementedException(); + } + + public override string GetName(int ordinal) + { + throw new NotImplementedException(); + } + + public override long GetInt64(int ordinal) + { + throw new NotImplementedException(); + } + + public override int GetInt32(int ordinal) + { + throw new NotImplementedException(); + } + + public override short GetInt16(int ordinal) + { + throw new NotImplementedException(); + } + + public override Guid GetGuid(int ordinal) + { + throw new NotImplementedException(); + } + + public override float GetFloat(int ordinal) + { + throw new NotImplementedException(); + } + + public override Type GetFieldType(int ordinal) + { + throw new NotImplementedException(); + } + + public override string GetString(int ordinal) + { + throw new NotImplementedException(); + } + + public override int GetValues(object[] values) + { + throw new NotImplementedException(); + } + + public override bool IsDBNull(int ordinal) + { + throw new NotImplementedException(); + } + + public override IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + public override int Depth { get; } + public override bool IsClosed { get; } + public override int RecordsAffected { get; } + + #endregion + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs index cda0ed5a..5ca94d2b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestObjects.cs @@ -18,6 +18,7 @@ using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Xunit; namespace Microsoft.SqlTools.Test.Utility @@ -97,179 +98,18 @@ namespace Microsoft.SqlTools.Test.Utility } } - public class TestDataReader : DbDataReader - { - - #region Test Specific Implementations - - internal string SqlCommandText { get; set; } - - private const string tableNameTestCommand = "SELECT name FROM sys.tables"; - - private List> tableNamesTest = new List> - { - new Dictionary { {"name", "table1"} }, - new Dictionary { {"name", "table2"} } - }; - - private IEnumerator> tableEnumerator; - - #endregion - - public override bool GetBoolean(int ordinal) - { - throw new NotImplementedException(); - } - - public override byte GetByte(int ordinal) - { - throw new NotImplementedException(); - } - - public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) - { - throw new NotImplementedException(); - } - - public override char GetChar(int ordinal) - { - throw new NotImplementedException(); - } - - public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) - { - throw new NotImplementedException(); - } - - public override string GetDataTypeName(int ordinal) - { - throw new NotImplementedException(); - } - - public override DateTime GetDateTime(int ordinal) - { - throw new NotImplementedException(); - } - - public override decimal GetDecimal(int ordinal) - { - throw new NotImplementedException(); - } - - public override double GetDouble(int ordinal) - { - throw new NotImplementedException(); - } - - public override IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - public override int GetOrdinal(string name) - { - throw new NotImplementedException(); - } - - public override string GetName(int ordinal) - { - throw new NotImplementedException(); - } - - public override long GetInt64(int ordinal) - { - throw new NotImplementedException(); - } - - public override int GetInt32(int ordinal) - { - throw new NotImplementedException(); - } - - public override short GetInt16(int ordinal) - { - throw new NotImplementedException(); - } - - public override Guid GetGuid(int ordinal) - { - throw new NotImplementedException(); - } - - public override float GetFloat(int ordinal) - { - throw new NotImplementedException(); - } - - public override Type GetFieldType(int ordinal) - { - throw new NotImplementedException(); - } - - public override string GetString(int ordinal) - { - throw new NotImplementedException(); - } - - public override object GetValue(int ordinal) - { - throw new NotImplementedException(); - } - - public override int GetValues(object[] values) - { - throw new NotImplementedException(); - } - - public override bool IsDBNull(int ordinal) - { - throw new NotImplementedException(); - } - - public override bool NextResult() - { - throw new NotImplementedException(); - } - - public override bool Read() - { - if (tableEnumerator == null) - { - switch (SqlCommandText) - { - case tableNameTestCommand: - tableEnumerator = ((IEnumerable>)tableNamesTest).GetEnumerator(); - break; - default: - throw new NotImplementedException(); - } - } - return tableEnumerator.MoveNext(); - } - - public override int Depth { get; } - public override bool IsClosed { get; } - public override int RecordsAffected { get; } - - public override object this[string name] - { - get { return tableEnumerator.Current[name]; } - } - - public override object this[int ordinal] - { - get { return tableEnumerator.Current[tableEnumerator.Current.Keys.ToArray()[ordinal]]; } - } - - public override int FieldCount { get; } - public override bool HasRows { get; } - } - /// /// Test mock class for IDbCommand /// public class TestSqlCommand : DbCommand { + internal TestSqlCommand(Dictionary[][] data) + { + Data = data; + } + + internal Dictionary[][] Data { get; set; } + public override void Cancel() { throw new NotImplementedException(); @@ -306,7 +146,7 @@ namespace Microsoft.SqlTools.Test.Utility protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { - return new TestDataReader {SqlCommandText = CommandText}; + return new TestDbDataReader(Data); } } @@ -315,6 +155,13 @@ namespace Microsoft.SqlTools.Test.Utility /// public class TestSqlConnection : DbConnection { + internal TestSqlConnection(Dictionary[][] data) + { + Data = data; + } + + internal Dictionary[][] Data { get; set; } + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { throw new NotImplementedException(); @@ -342,7 +189,7 @@ namespace Microsoft.SqlTools.Test.Utility protected override DbCommand CreateDbCommand() { - return new TestSqlCommand(); + return new TestSqlCommand(Data); } public override void ChangeDatabase(string databaseName) @@ -358,7 +205,7 @@ namespace Microsoft.SqlTools.Test.Utility { public DbConnection CreateSqlConnection(string connectionString) { - return new TestSqlConnection() + return new TestSqlConnection(null) { ConnectionString = connectionString }; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json index 3d023cd4..23c97d0b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/project.json +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/project.json @@ -14,7 +14,8 @@ "moq.netcore": "4.4.0-beta8", "Microsoft.SqlTools.ServiceLayer": { "target": "project" - } + }, + "System.Diagnostics.TraceSource": "4.0.0" }, "testRunner": "xunit", "frameworks": { From d783fd505bc8d00e1856c42469aa53c5ebd98663 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 9 Aug 2016 11:10:54 -0700 Subject: [PATCH 10/12] Second batch of unit tests Making slight changes to RequestContext to make it easier to mock --- .../Hosting/Protocol/RequestContext.cs | 8 +- .../QueryExecution/QueryExecutionService.cs | 20 ++- .../QueryExecution/Common.cs | 142 ++++++++++++++++++ .../QueryExecution/ExecuteTests.cs | 137 +++++------------ .../QueryExecution/ServiceTests.cs | 138 +++++++++++++++++ .../QueryExecution/SubsetTests.cs | 52 ++++++- .../Utility/TestDbDataReader.cs | 8 - 7 files changed, 387 insertions(+), 118 deletions(-) create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/RequestContext.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/RequestContext.cs index 153e46d6..a2811f6a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/RequestContext.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Protocol/RequestContext.cs @@ -20,7 +20,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol this.messageWriter = messageWriter; } - public async Task SendResult(TResult resultDetails) + public RequestContext() { } + + public virtual async Task SendResult(TResult resultDetails) { await this.messageWriter.WriteResponse( resultDetails, @@ -28,14 +30,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol requestMessage.Id); } - public async Task SendEvent(EventType eventType, TParams eventParams) + public virtual async Task SendEvent(EventType eventType, TParams eventParams) { await this.messageWriter.WriteEvent( eventType, eventParams); } - public async Task SendError(object errorDetails) + public virtual async Task SendError(object errorDetails) { await this.messageWriter.WriteMessage( Message.ResponseError( diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index e9f0ba1a..540390ee 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -19,7 +19,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution get { return instance.Value; } } - private QueryExecutionService() { } + private QueryExecutionService() + { + ConnectionService = ConnectionService.Instance; + } + + internal QueryExecutionService(ConnectionService connService) + { + ConnectionService = connService; + } #endregion @@ -33,6 +41,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution get { return queries.Value; } } + private ConnectionService ConnectionService { get; set; } + #endregion #region Public Methods @@ -55,12 +65,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution #region Request Handlers - private async Task HandleExecuteRequest(QueryExecuteParams executeParams, + public async Task HandleExecuteRequest(QueryExecuteParams executeParams, RequestContext requestContext) { // Attempt to get the connection for the editor ConnectionInfo connectionInfo; - if(!ConnectionService.Instance.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + if(!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) { await requestContext.SendError("This editor is not connected to a database."); return; @@ -94,7 +104,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); } - private async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, + public async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, RequestContext requestContext) { // Attempt to load the query @@ -129,7 +139,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } - private async Task HandleDisposeRequest(QueryDisposeParams disposeParams, + public async Task HandleDisposeRequest(QueryDisposeParams disposeParams, RequestContext requestContext) { // Attempt to remove the query for the owner uri diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs new file mode 100644 index 00000000..144f3526 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.Test.Utility; +using Moq; +using Moq.Protected; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class Common + { + public static readonly 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"} }, + }; + + public static Dictionary[] GetTestData(int columns, int rows) + { + Dictionary[] output = new Dictionary[rows]; + for (int row = 0; row < rows; row++) + { + Dictionary rowDictionary = new Dictionary(); + for (int column = 0; column < columns; column++) + { + rowDictionary.Add(String.Format("column{0}", column), String.Format("val{0}{1}", column, row)); + } + output[row] = rowDictionary; + } + + return output; + } + + public static Query GetBasicExecutedQuery() + { + Query query = new Query("SIMPLE QUERY", CreateTestConnectionInfo(new[] { StandardTestData }, false)); + query.Execute().Wait(); + return query; + } + + #region Mocking + + //private static DbDataReader CreateTestReader(int columnCount, int rowCount) + //{ + // var readerMock = new Mock { CallBase = true }; + + // // Setup for column reads + // // TODO: We can't test columns because of oddities with how datatable/GetColumn + + // // Setup for row reads + // var readSequence = readerMock.SetupSequence(dbReader => dbReader.Read()); + // for (int i = 0; i < rowCount; i++) + // { + // readSequence.Returns(true); + // } + // readSequence.Returns(false); + + // // Make sure that if we call for data from the reader it works + // readerMock.Setup(dbReader => dbReader[InColumnRange(columnCount)]) + // .Returns(i => i.ToString()); + // readerMock.Setup(dbReader => dbReader[NotInColumnRange(columnCount)]) + // .Throws(new ArgumentOutOfRangeException()); + // readerMock.Setup(dbReader => dbReader.HasRows) + // .Returns(rowCount > 0); + + // return readerMock.Object; + //} + + //private static int InColumnRange(int columnCount) + //{ + // return Match.Create(i => i < columnCount && i > 0); + //} + + //private static int NotInColumnRange(int columnCount) + //{ + // return Match.Create(i => i >= columnCount || i < 0); + //} + + public static DbCommand CreateTestCommand(Dictionary[][] data, bool throwOnRead) + { + var commandMock = new Mock { CallBase = true }; + var commandMockSetup = commandMock.Protected() + .Setup("ExecuteDbDataReader", It.IsAny()); + + // Setup the expected behavior + if (throwOnRead) + { + commandMockSetup.Throws(new Mock().Object); + } + else + { + commandMockSetup.Returns(new TestDbDataReader(data)); + } + + + return commandMock.Object; + } + + public static DbConnection CreateTestConnection(Dictionary[][] data, bool throwOnRead) + { + var connectionMock = new Mock { CallBase = true }; + connectionMock.Protected() + .Setup("CreateDbCommand") + .Returns(CreateTestCommand(data, throwOnRead)); + + return connectionMock.Object; + } + + public static ISqlConnectionFactory CreateMockFactory(Dictionary[][] data, bool throwOnRead) + { + var mockFactory = new Mock(); + mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny())) + .Returns(CreateTestConnection(data, throwOnRead)); + + return mockFactory.Object; + } + + public static ConnectionInfo CreateTestConnectionInfo(Dictionary[][] data, bool throwOnRead) + { + // Create connection info + ConnectionDetails connDetails = new ConnectionDetails + { + UserName = "sa", + Password = "Yukon900", + DatabaseName = "AdventureWorks2016CTP3_2", + ServerName = "sqltools11" + }; + + return new ConnectionInfo(CreateMockFactory(data, throwOnRead), "test://test", connDetails); + } + + #endregion + + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index 3eb923c5..b9eebfd4 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,35 +1,18 @@ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Data; -using System.Data.Common; using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; -using Microsoft.SqlTools.ServiceLayer.Test.Utility; -using Moq; -using Moq.Protected; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class ExecuteTests { - private static Dictionary[] testData = - { - 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"} }, - }; - [Fact] public void QueryCreationTest() { // If I create a new query... - Query query = new Query("NO OP", CreateTestConnectionInfo(null)); + Query query = new Query("NO OP", Common.CreateTestConnectionInfo(null, false)); // Then: // ... It should not have executed @@ -44,7 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution public void QueryExecuteNoResultSets() { // If I execute a query that should get no result sets - Query query = new Query("Query with no result sets", CreateTestConnectionInfo(null)); + Query query = new Query("Query with no result sets", Common.CreateTestConnectionInfo(null, false)); query.Execute().Wait(); // Then: @@ -63,7 +46,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution [Fact] public void QueryExecuteQueryOneResultSet() { - ConnectionInfo ci = CreateTestConnectionInfo(new[] {testData}); + ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] {Common.StandardTestData}, false); // If I execute a query that should get one result set int resultSets = 1; @@ -99,11 +82,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution [Fact] public void QueryExecuteQueryTwoResultSets() { - var dataset = new[] {testData, testData}; + var dataset = new[] {Common.StandardTestData, Common.StandardTestData}; int resultSets = dataset.Length; - int rows = testData.Length; - int columns = testData[0].Count; - ConnectionInfo ci = CreateTestConnectionInfo(dataset); + 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); @@ -139,87 +122,43 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution } } - #region Mocking - - //private static DbDataReader CreateTestReader(int columnCount, int rowCount) - //{ - // var readerMock = new Mock { CallBase = true }; - - // // Setup for column reads - // // TODO: We can't test columns because of oddities with how datatable/GetColumn - - // // Setup for row reads - // var readSequence = readerMock.SetupSequence(dbReader => dbReader.Read()); - // for (int i = 0; i < rowCount; i++) - // { - // readSequence.Returns(true); - // } - // readSequence.Returns(false); - - // // Make sure that if we call for data from the reader it works - // readerMock.Setup(dbReader => dbReader[InColumnRange(columnCount)]) - // .Returns(i => i.ToString()); - // readerMock.Setup(dbReader => dbReader[NotInColumnRange(columnCount)]) - // .Throws(new ArgumentOutOfRangeException()); - // readerMock.Setup(dbReader => dbReader.HasRows) - // .Returns(rowCount > 0); - - // return readerMock.Object; - //} - - //private static int InColumnRange(int columnCount) - //{ - // return Match.Create(i => i < columnCount && i > 0); - //} - - //private static int NotInColumnRange(int columnCount) - //{ - // return Match.Create(i => i >= columnCount || i < 0); - //} - - private static DbCommand CreateTestCommand(Dictionary[][] data) + [Fact] + public void QueryExecuteInvalidQuery() { - var commandMock = new Mock {CallBase = true}; - commandMock.Protected() - .Setup("ExecuteDbDataReader", It.IsAny()) - .Returns(new TestDbDataReader(data)); + ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); - return commandMock.Object; + // If I execute a query that is invalid + Query query = new Query("Invalid query", ci); + + // Then: + // ... It should throw an exception + Exception e = Assert.Throws(() => query.Execute().Wait()); } - private static DbConnection CreateTestConnection(Dictionary[][] data) + [Fact] + public void QueryExecuteExecutedQuery() { - var connectionMock = new Mock {CallBase = true}; - connectionMock.Protected() - .Setup("CreateDbCommand") - .Returns(CreateTestCommand(data)); + ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] {Common.StandardTestData}, false); - return connectionMock.Object; + // If I execute a query + Query query = new Query("Any query", ci); + query.Execute().Wait(); + + // Then: + // ... It should have executed + Assert.True(query.HasExecuted, "The query should have been marked executed."); + + // 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]); + + // ... The data should still be available + Assert.True(query.HasExecuted, "The query should still be marked executed."); + Assert.NotEmpty(query.ResultSets); + Assert.NotEmpty(query.ResultSummary); } - - private static ISqlConnectionFactory CreateMockFactory(Dictionary[][] data) - { - var mockFactory = new Mock(); - mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny())) - .Returns(CreateTestConnection(data)); - - return mockFactory.Object; - } - - private static ConnectionInfo CreateTestConnectionInfo(Dictionary[][] data) - { - // Create connection info - ConnectionDetails connDetails = new ConnectionDetails - { - UserName = "sa", - Password = "Yukon900", - DatabaseName = "AdventureWorks2016CTP3_2", - ServerName = "sqltools11" - }; - - return new ConnectionInfo(CreateMockFactory(data), "test://test", connDetails); - } - - #endregion } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs new file mode 100644 index 00000000..58e5800f --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +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.Test.Utility; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution +{ + public class ServiceTests + { + + [Fact] + public void QueryExecuteValidNoResultsTest() + { + // If: + // ... I request to execute a valid query with no results + var queryService = GetPrimedExecutionService(Common.CreateMockFactory(null, false)); + var queryParams = new QueryExecuteParams + { + QueryText = "Doesn't Matter", + OwnerUri = "testFile" + }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams completeParams = null; + var requestContext = GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No Errors should have been sent + // ... A successful result should have been sent with no messages + // ... A completion event should have been fired with empty results + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Empty(completeParams.Messages); + Assert.Empty(completeParams.ResultSetSummaries); + Assert.False(completeParams.Error); + } + + [Fact] + public void QueryExecuteValidResultsTest() + { + // If: + // ... I request to execute a valid query with results + var queryService = GetPrimedExecutionService(Common.CreateMockFactory(new [] {Common.StandardTestData}, false)); + var queryParams = new QueryExecuteParams {OwnerUri = "testFile", QueryText = "Doesn't Matter"}; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams completeParams = null; + var requestContext = GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No errors should have been send + // ... A successful result should have been sent with no messages + // ... A completion event should hvae been fired with one result + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Empty(completeParams.Messages); + Assert.NotEmpty(completeParams.ResultSetSummaries); + Assert.False(completeParams.Error); + } + + + + private ConnectionDetails GetTestConnectionDetails() + { + return new ConnectionDetails + { + DatabaseName = "123", + Password = "456", + ServerName = "789", + UserName = "012" + }; + } + + private QueryExecutionService GetPrimedExecutionService(ISqlConnectionFactory factory) + { + var connectionService = new ConnectionService(factory); + connectionService.Connect(new ConnectParams {Connection = GetTestConnectionDetails(), OwnerUri = "testFile"}); + return new QueryExecutionService(connectionService); + } + + private Mock> GetQueryExecuteResultContextMock( + Action resultCallback, + Action, QueryExecuteCompleteParams> eventCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendEvent + var sendEventFlow = requestContext.Setup(rc => rc.SendEvent( + It.Is>(m => m == QueryExecuteCompleteEvent.Type), + It.IsAny())) + .Returns(Task.FromResult(0)); + if (eventCallback != null) + { + sendEventFlow.Callback(eventCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext.Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + private void VerifyQueryExecuteCallCount(Mock> mock, Times sendResultCalls, Times sendEventCalls, Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendEvent( + It.Is>(m => m == QueryExecuteCompleteEvent.Type), + It.IsAny()), sendEventCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index f64dd96c..c89b643f 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -1,11 +1,57 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class SubsetTests { + [Theory] + [InlineData(2)] + [InlineData(20)] + public void SubsetValidTest(int rowCount) + { + // If I have an executed query + Query q = Common.GetBasicExecutedQuery(); + + // ... And I ask for a subset with valid arguments + ResultSetSubset subset = q.GetSubset(0, 0, rowCount); + + // Then: + // I should get the requested number of rows + Assert.Equal(Math.Min(rowCount, Common.StandardTestData.Length), subset.RowCount); + 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)); + + // ... And I ask for a subset with valid arguments + // Then: + // ... It should throw an exception + Assert.Throws(() => q.GetSubset(0, 0, 2)); + } + + [Theory] + [InlineData(-1, 0, 2)] // Invalid result set, too low + [InlineData(2, 0, 2)] // Invalid result set, too high + [InlineData(0, -1, 2)] // Invalid start index, too low + [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) + { + // If I have an executed query + Query q = Common.GetBasicExecutedQuery(); + + // ... 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)); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs index 0031ad4a..69edef72 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Utility/TestDbDataReader.cs @@ -23,14 +23,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Utility private IEnumerator> Rows { get; set; } - private const string tableNameTestCommand = "SELECT name FROM sys.tables"; - - private List> tableNamesTest = new List> - { - new Dictionary { {"name", "table1"} }, - new Dictionary { {"name", "table2"} } - }; - public TestDbDataReader(Dictionary[][] data) { Data = data; From 8167330e16947bd7204a4578566ca3d7d490ea3e Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 10 Aug 2016 15:14:56 -0700 Subject: [PATCH 11/12] Finishing up unit tests --- .../QueryExecution/Query.cs | 71 +++--- .../QueryExecution/QueryExecutionService.cs | 205 +++++++++++----- .../QueryExecution/Common.cs | 113 ++++++--- .../QueryExecution/DisposeTests.cs | 86 ++++++- .../QueryExecution/ExecuteTests.cs | 222 ++++++++++++++++++ .../QueryExecution/ServiceTests.cs | 138 ----------- .../QueryExecution/SubsetTests.cs | 149 ++++++++++++ 7 files changed, 718 insertions(+), 266 deletions(-) delete mode 100644 test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 831e981b..eeaa8903 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -42,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public Query(string queryText, ConnectionInfo connection) { // Sanity check for input - if (queryText == null) + if (String.IsNullOrWhiteSpace(queryText)) { throw new ArgumentNullException(nameof(queryText), "Query text cannot be null"); } @@ -68,50 +68,55 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } // Create a connection from the connection details - string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); - using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) + try { - await conn.OpenAsync(cancellationSource.Token); - - // Create a command that we'll use for executing the query - using (DbCommand command = conn.CreateCommand()) + string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); + using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) { - command.CommandText = QueryText; - command.CommandType = CommandType.Text; + await conn.OpenAsync(cancellationSource.Token); - // Execute the command to get back a reader - using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationSource.Token)) + // Create a command that we'll use for executing the query + using (DbCommand command = conn.CreateCommand()) { - do + command.CommandText = QueryText; + command.CommandType = CommandType.Text; + + // Execute the command to get back a reader + using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationSource.Token)) { - // TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows - if (!reader.HasRows) + do { - continue; - } + // TODO: This doesn't properly handle scenarios where the query is SELECT but does not have rows + if (!reader.HasRows) + { + 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 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(); - } + // 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)); + // Add the result set to the results of the query + ResultSets.Add(resultSet); + } while (await reader.NextResultAsync(cancellationSource.Token)); + } } } } - - // Mark that we have executed - HasExecuted = true; + finally + { + // Mark that we have executed + HasExecuted = true; + } } public ResultSetSubset GetSubset(int resultSetIndex, int startRow, int rowCount) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 540390ee..b98b5ac8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -1,5 +1,11 @@ -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.Concurrent; +using System.Data.Common; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; @@ -36,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution private readonly Lazy> queries = new Lazy>(() => new ConcurrentDictionary()); - private ConcurrentDictionary ActiveQueries + internal ConcurrentDictionary ActiveQueries { get { return queries.Value; } } @@ -68,59 +74,37 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution public async Task HandleExecuteRequest(QueryExecuteParams executeParams, RequestContext requestContext) { - // Attempt to get the connection for the editor - ConnectionInfo connectionInfo; - if(!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + try { - await requestContext.SendError("This editor is not connected to a database."); - return; + // Get a query new active query + Query newQuery = await CreateAndActivateNewQuery(executeParams, requestContext); + + // Execute the query + await ExecuteAndCompleteQuery(executeParams, requestContext, newQuery); } - - // If there is already an in-flight query, error out - Query newQuery = new Query(executeParams.QueryText, connectionInfo); - if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) + catch (Exception e) { - await requestContext.SendError("A query is already in progress for this editor session." + - "Please cancel this query or wait for its completion."); - return; + // Dump any unexpected exceptions as errors + await requestContext.SendError(e.Message); } - - // Launch the query and respond with successfully launching it - Task executeTask = newQuery.Execute(); - await requestContext.SendResult(new QueryExecuteResult - { - Messages = null - }); - - // Wait for query execution and then send back the results - await Task.WhenAll(executeTask); - QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams - { - Error = false, - Messages = new string[]{}, // TODO: Figure out how to get messages back from the server - OwnerUri = executeParams.OwnerUri, - ResultSetSummaries = newQuery.ResultSummary - }; - await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); } public async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams, RequestContext requestContext) { - // Attempt to load the query - Query query; - if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) - { - var errorResult = new QueryExecuteSubsetResult - { - Message = "The requested query does not exist." - }; - await requestContext.SendResult(errorResult); - return; - } - try { + // Attempt to load the query + Query query; + if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) + { + await requestContext.SendResult(new QueryExecuteSubsetResult + { + Message = "The requested query does not exist." + }); + return; + } + // Retrieve the requested subset and return it var result = new QueryExecuteSubsetResult { @@ -130,34 +114,143 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution }; await requestContext.SendResult(result); } - catch (Exception e) + catch (InvalidOperationException ioe) { + // Return the error as a result await requestContext.SendResult(new QueryExecuteSubsetResult { - Message = e.Message + Message = ioe.Message }); } + catch (ArgumentOutOfRangeException aoore) + { + // Return the error as a result + await requestContext.SendResult(new QueryExecuteSubsetResult + { + Message = aoore.Message + }); + } + catch (Exception e) + { + // This was unexpected, so send back as error + await requestContext.SendError(e.Message); + } } public async Task HandleDisposeRequest(QueryDisposeParams disposeParams, RequestContext requestContext) { - // Attempt to remove the query for the owner uri - Query result; - if (!ActiveQueries.TryRemove(disposeParams.OwnerUri, out result)) + try { - await requestContext.SendError("Failed to dispose query, ID not found."); - return; - } + // Attempt to remove the query for the owner uri + Query result; + if (!ActiveQueries.TryRemove(disposeParams.OwnerUri, out result)) + { + await requestContext.SendResult(new QueryDisposeResult + { + Messages = "Failed to dispose query, ID not found." + }); + return; + } - // Success - await requestContext.SendResult(new QueryDisposeResult + // Success + await requestContext.SendResult(new QueryDisposeResult + { + Messages = null + }); + } + catch (Exception e) { - Messages = null - }); + await requestContext.SendError(e.Message); + } } #endregion + private async Task CreateAndActivateNewQuery(QueryExecuteParams executeParams, RequestContext requestContext) + { + try + { + // Attempt to get the connection for the editor + ConnectionInfo connectionInfo; + if (!ConnectionService.TryFindConnection(executeParams.OwnerUri, out connectionInfo)) + { + await requestContext.SendResult(new QueryExecuteResult + { + Messages = "This editor is not connected to a database." + }); + return null; + } + + // Attempt to clean out any old query on the owner URI + Query oldQuery; + if (ActiveQueries.TryGetValue(executeParams.OwnerUri, out oldQuery) && oldQuery.HasExecuted) + { + ActiveQueries.TryRemove(executeParams.OwnerUri, out oldQuery); + } + + // If we can't add the query now, it's assumed the query is in progress + Query newQuery = new Query(executeParams.QueryText, connectionInfo); + if (!ActiveQueries.TryAdd(executeParams.OwnerUri, newQuery)) + { + await requestContext.SendResult(new QueryExecuteResult + { + Messages = "A query is already in progress for this editor session." + + "Please cancel this query or wait for its completion." + }); + return null; + } + + return newQuery; + } + catch (ArgumentNullException ane) + { + await requestContext.SendResult(new QueryExecuteResult { Messages = ane.Message }); + return null; + } + // Any other exceptions will fall through here and be collected at the end + } + + private async Task ExecuteAndCompleteQuery(QueryExecuteParams executeParams, RequestContext requestContext, Query query) + { + // Skip processing if the query is null + if (query == null) + { + return; + } + + // Launch the query and respond with successfully launching it + Task executeTask = query.Execute(); + await requestContext.SendResult(new QueryExecuteResult + { + Messages = null + }); + + try + { + // Wait for query execution and then send back the results + await Task.WhenAll(executeTask); + QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams + { + Error = false, + Messages = new string[] { }, // TODO: Figure out how to get messages back from the server + OwnerUri = executeParams.OwnerUri, + ResultSetSummaries = query.ResultSummary + }; + await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); + } + catch (DbException dbe) + { + // Dump the message to a complete event + QueryExecuteCompleteParams errorEvent = new QueryExecuteCompleteParams + { + Error = true, + Messages = new[] {dbe.Message}, + OwnerUri = executeParams.OwnerUri, + ResultSetSummaries = query.ResultSummary + }; + await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, errorEvent); + } + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs index 144f3526..9bc8053b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Common.cs @@ -2,9 +2,13 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +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.Test.Utility; using Moq; using Moq.Protected; @@ -13,6 +17,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class Common { + public const string OwnerUri = "testFile"; + public static readonly Dictionary[] StandardTestData = { new Dictionary { {"col1", "val11"}, { "col2", "val12"}, { "col3", "val13"}, { "col4", "col14"} }, @@ -45,43 +51,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution return query; } - #region Mocking - - //private static DbDataReader CreateTestReader(int columnCount, int rowCount) - //{ - // var readerMock = new Mock { CallBase = true }; - - // // Setup for column reads - // // TODO: We can't test columns because of oddities with how datatable/GetColumn - - // // Setup for row reads - // var readSequence = readerMock.SetupSequence(dbReader => dbReader.Read()); - // for (int i = 0; i < rowCount; i++) - // { - // readSequence.Returns(true); - // } - // readSequence.Returns(false); - - // // Make sure that if we call for data from the reader it works - // readerMock.Setup(dbReader => dbReader[InColumnRange(columnCount)]) - // .Returns(i => i.ToString()); - // readerMock.Setup(dbReader => dbReader[NotInColumnRange(columnCount)]) - // .Throws(new ArgumentOutOfRangeException()); - // readerMock.Setup(dbReader => dbReader.HasRows) - // .Returns(rowCount > 0); - - // return readerMock.Object; - //} - - //private static int InColumnRange(int columnCount) - //{ - // return Match.Create(i => i < columnCount && i > 0); - //} - - //private static int NotInColumnRange(int columnCount) - //{ - // return Match.Create(i => i >= columnCount || i < 0); - //} + #region DbConnection Mocking public static DbCommand CreateTestCommand(Dictionary[][] data, bool throwOnRead) { @@ -138,5 +108,74 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution #endregion + #region Service Mocking + + public static ConnectionDetails GetTestConnectionDetails() + { + return new ConnectionDetails + { + DatabaseName = "123", + Password = "456", + ServerName = "789", + UserName = "012" + }; + } + + public static QueryExecutionService GetPrimedExecutionService(ISqlConnectionFactory factory, bool isConnected) + { + var connectionService = new ConnectionService(factory); + if (isConnected) + { + connectionService.Connect(new ConnectParams + { + Connection = GetTestConnectionDetails(), + OwnerUri = OwnerUri + }); + } + return new QueryExecutionService(connectionService); + } + + #endregion + + #region Request Mocking + + public static Mock> GetQueryExecuteResultContextMock( + Action resultCallback, + Action, QueryExecuteCompleteParams> eventCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendEvent + var sendEventFlow = requestContext.Setup(rc => rc.SendEvent( + It.Is>(m => m == QueryExecuteCompleteEvent.Type), + It.IsAny())) + .Returns(Task.FromResult(0)); + if (eventCallback != null) + { + sendEventFlow.Callback(eventCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext.Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + #endregion } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs index def3c6b6..c0fed697 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/DisposeTests.cs @@ -1,11 +1,93 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Moq; +using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class DisposeTests { + [Fact] + public void DisposeExecutedQuery() + { + // If: + // ... I request a query (doesn't matter what kind) + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams {QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri}; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // ... And then I dispose of the query + var disposeParams = new QueryDisposeParams {OwnerUri = Common.OwnerUri}; + QueryDisposeResult result = null; + var disposeRequest = GetQueryDisposeResultContextMock(qdr => result = qdr, null); + queryService.HandleDisposeRequest(disposeParams, disposeRequest.Object).Wait(); + + // Then: + // ... I should have seen a successful result + // ... And the active queries should be empty + VerifyQueryDisposeCallCount(disposeRequest, Times.Once(), Times.Never()); + Assert.Null(result.Messages); + Assert.Empty(queryService.ActiveQueries); + } + + [Fact] + public void QueryDisposeMissingQuery() + { + // If: + // ... I attempt to dispose a query that doesn't exist + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), false); + var disposeParams = new QueryDisposeParams {OwnerUri = Common.OwnerUri}; + QueryDisposeResult result = null; + var disposeRequest = GetQueryDisposeResultContextMock(qdr => result = qdr, null); + queryService.HandleDisposeRequest(disposeParams, disposeRequest.Object).Wait(); + + // Then: + // ... I should have gotten an error result + VerifyQueryDisposeCallCount(disposeRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + } + + #region Mocking + + private Mock> GetQueryDisposeResultContextMock( + Action resultCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext + .Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + private void VerifyQueryDisposeCallCount(Mock> mock, Times sendResultCalls, + Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } + + #endregion + } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index b9eebfd4..ffc0a870 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -1,13 +1,18 @@ using System; 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 Moq; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class ExecuteTests { + #region Query Class Tests + [Fact] public void QueryCreationTest() { @@ -160,5 +165,222 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.NotEmpty(query.ResultSets); Assert.NotEmpty(query.ResultSummary); } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void QueryExecuteNoQuery(string query) + { + // If: + // ... I create a query that has an empty query + // Then: + // ... It should throw an exception + Assert.Throws(() => new Query(query, null)); + } + + [Fact] + public void QueryExecuteNoConnectionInfo() + { + // If: + // ... I create a query that has a null connection info + // Then: + // ... It should throw an exception + Assert.Throws(() => new Query("Some Query", null)); + } + + #endregion + + #region Service Tests + + [Fact] + public void QueryExecuteValidNoResultsTest() + { + // 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 }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams completeParams = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No Errors should have been sent + // ... A successful result should have been sent with no 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.Empty(completeParams.Messages); + Assert.Empty(completeParams.ResultSetSummaries); + Assert.False(completeParams.Error); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [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" }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams completeParams = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... No errors should have been sent + // ... A successful result should have been sent with no 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.Empty(completeParams.Messages); + Assert.NotEmpty(completeParams.ResultSetSummaries); + Assert.False(completeParams.Error); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [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" }; + + QueryExecuteResult result = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... An error message should have been returned via the result + // ... 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); + } + + [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" }; + + // 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(); + + // 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] + 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" }; + + // 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 after waiting for the first to complete + QueryExecuteResult result = null; + QueryExecuteCompleteParams complete = null; + var secondRequestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, qecp) => complete = qecp, null); + queryService.HandleExecuteRequest(queryParams, secondRequestContext.Object).Wait(); + + // Then: + // ... 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.Error); + Assert.Equal(1, queryService.ActiveQueries.Count); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void QueryExecuteMissingQueryTest(string query) + { + // If: + // ... I request to execute a query with a missing query string + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QueryText = query }; + + QueryExecuteResult result = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, null, null); + queryService.HandleExecuteRequest(queryParams, requestContext.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 + VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Never(), Times.Never()); + Assert.NotNull(result.Messages); + Assert.NotEmpty(result.Messages); + } + + [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!" }; + + QueryExecuteResult result = null; + QueryExecuteCompleteParams complete = null; + var requestContext = Common.GetQueryExecuteResultContextMock(qer => result = qer, (et, qecp) => complete = qecp, null); + queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); + + // Then: + // ... 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.Error); + Assert.NotEmpty(complete.Messages); + } + + #endregion + + private void VerifyQueryExecuteCallCount(Mock> mock, Times sendResultCalls, Times sendEventCalls, Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendEvent( + It.Is>(m => m == QueryExecuteCompleteEvent.Type), + It.IsAny()), sendEventCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs deleted file mode 100644 index 58e5800f..00000000 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ServiceTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.SqlTools.ServiceLayer.Connection; -using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; -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.Test.Utility; -using Moq; -using Xunit; - -namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution -{ - public class ServiceTests - { - - [Fact] - public void QueryExecuteValidNoResultsTest() - { - // If: - // ... I request to execute a valid query with no results - var queryService = GetPrimedExecutionService(Common.CreateMockFactory(null, false)); - var queryParams = new QueryExecuteParams - { - QueryText = "Doesn't Matter", - OwnerUri = "testFile" - }; - - QueryExecuteResult result = null; - QueryExecuteCompleteParams completeParams = null; - var requestContext = GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); - queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); - - // Then: - // ... No Errors should have been sent - // ... A successful result should have been sent with no messages - // ... A completion event should have been fired with empty results - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.Empty(completeParams.Messages); - Assert.Empty(completeParams.ResultSetSummaries); - Assert.False(completeParams.Error); - } - - [Fact] - public void QueryExecuteValidResultsTest() - { - // If: - // ... I request to execute a valid query with results - var queryService = GetPrimedExecutionService(Common.CreateMockFactory(new [] {Common.StandardTestData}, false)); - var queryParams = new QueryExecuteParams {OwnerUri = "testFile", QueryText = "Doesn't Matter"}; - - QueryExecuteResult result = null; - QueryExecuteCompleteParams completeParams = null; - var requestContext = GetQueryExecuteResultContextMock(qer => result = qer, (et, cp) => completeParams = cp, null); - queryService.HandleExecuteRequest(queryParams, requestContext.Object).Wait(); - - // Then: - // ... No errors should have been send - // ... A successful result should have been sent with no messages - // ... A completion event should hvae been fired with one result - VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); - Assert.Null(result.Messages); - Assert.Empty(completeParams.Messages); - Assert.NotEmpty(completeParams.ResultSetSummaries); - Assert.False(completeParams.Error); - } - - - - private ConnectionDetails GetTestConnectionDetails() - { - return new ConnectionDetails - { - DatabaseName = "123", - Password = "456", - ServerName = "789", - UserName = "012" - }; - } - - private QueryExecutionService GetPrimedExecutionService(ISqlConnectionFactory factory) - { - var connectionService = new ConnectionService(factory); - connectionService.Connect(new ConnectParams {Connection = GetTestConnectionDetails(), OwnerUri = "testFile"}); - return new QueryExecutionService(connectionService); - } - - private Mock> GetQueryExecuteResultContextMock( - Action resultCallback, - Action, QueryExecuteCompleteParams> eventCallback, - Action errorCallback) - { - var requestContext = new Mock>(); - - // Setup the mock for SendResult - var sendResultFlow = requestContext - .Setup(rc => rc.SendResult(It.IsAny())) - .Returns(Task.FromResult(0)); - if (resultCallback != null) - { - sendResultFlow.Callback(resultCallback); - } - - // Setup the mock for SendEvent - var sendEventFlow = requestContext.Setup(rc => rc.SendEvent( - It.Is>(m => m == QueryExecuteCompleteEvent.Type), - It.IsAny())) - .Returns(Task.FromResult(0)); - if (eventCallback != null) - { - sendEventFlow.Callback(eventCallback); - } - - // Setup the mock for SendError - var sendErrorFlow = requestContext.Setup(rc => rc.SendError(It.IsAny())) - .Returns(Task.FromResult(0)); - if (errorCallback != null) - { - sendErrorFlow.Callback(errorCallback); - } - - return requestContext; - } - - private void VerifyQueryExecuteCallCount(Mock> mock, Times sendResultCalls, Times sendEventCalls, Times sendErrorCalls) - { - mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); - mock.Verify(rc => rc.SendEvent( - It.Is>(m => m == QueryExecuteCompleteEvent.Type), - It.IsAny()), sendEventCalls); - mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); - } - } -} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs index c89b643f..bdb0dc48 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SubsetTests.cs @@ -1,12 +1,17 @@ using System; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Moq; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution { public class SubsetTests { + #region Query Class Tests + [Theory] [InlineData(2)] [InlineData(20)] @@ -53,5 +58,149 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... It should throw an exception Assert.Throws(() => q.GetSubset(resultSetIndex, rowStartInex, rowCount)); } + + #endregion + + #region Service Intergration Tests + + [Fact] + public void SubsetServiceValidTest() + { + // If: + // ... I have a query that has results (doesn't matter what) + var queryService =Common.GetPrimedExecutionService( + Common.CreateMockFactory(new[] {Common.StandardTestData}, false), true); + var executeParams = new QueryExecuteParams {QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri}; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // ... 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}; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should have a successful result + // ... There should be rows there (other test validate that the rows are correct) + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.Null(result.Message); + Assert.NotNull(result.ResultSubset); + } + + [Fact] + public void SubsetServiceMissingQueryTest() + { + // If: + // ... I ask for a set of results for a file that hasn't executed a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should have an error result + // ... There should be no rows in the result set + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Message); + Assert.Null(result.ResultSubset); + } + + [Fact] + public void SubsetServiceUnexecutedQueryTest() + { + // If: + // ... I have a query that hasn't finished executing (doesn't matter what) + var queryService = Common.GetPrimedExecutionService( + Common.CreateMockFactory(new[] { Common.StandardTestData }, false), true); + 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; + + // ... 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 }; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should get an error result + // ... There should not be rows + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Message); + Assert.Null(result.ResultSubset); + } + + [Fact] + public void SubsetServiceOutOfRangeSubsetTest() + { + // If: + // ... I have a query that doesn't have any result sets + var queryService = Common.GetPrimedExecutionService( + Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams { QueryText = "Doesn'tMatter", OwnerUri = Common.OwnerUri }; + var executeRequest = Common.GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // ... And I then ask for a set of results from it + var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; + QueryExecuteSubsetResult result = null; + var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); + queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); + + // Then: + // ... I should get an error result + // ... There should not be rows + // ... There should not be any error calls + VerifyQuerySubsetCallCount(subsetRequest, Times.Once(), Times.Never()); + Assert.NotNull(result.Message); + Assert.Null(result.ResultSubset); + } + + #endregion + + #region Mocking + + private Mock> GetQuerySubsetResultContextMock( + Action resultCallback, + Action errorCallback) + { + var requestContext = new Mock>(); + + // Setup the mock for SendResult + var sendResultFlow = requestContext + .Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + if (resultCallback != null) + { + sendResultFlow.Callback(resultCallback); + } + + // Setup the mock for SendError + var sendErrorFlow = requestContext + .Setup(rc => rc.SendError(It.IsAny())) + .Returns(Task.FromResult(0)); + if (errorCallback != null) + { + sendErrorFlow.Callback(errorCallback); + } + + return requestContext; + } + + private void VerifyQuerySubsetCallCount(Mock> mock, Times sendResultCalls, + Times sendErrorCalls) + { + mock.Verify(rc => rc.SendResult(It.IsAny()), sendResultCalls); + mock.Verify(rc => rc.SendError(It.IsAny()), sendErrorCalls); + } + + #endregion + } } From 68c25f506e505e8cffb36814eec29ec7185731ee Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 10 Aug 2016 16:40:36 -0700 Subject: [PATCH 12/12] Adding changes as requested for code review --- .../Contracts/QueryDisposeRequest.cs | 1 - .../QueryExecuteCompleteNotification.cs | 12 ++- .../Contracts/QueryExecuteRequest.cs | 1 - .../Contracts/QueryExecuteSubsetRequest.cs | 8 +- .../Contracts/ResultSetSubset.cs | 19 +++- .../Contracts/ResultSetSummary.cs | 10 +- .../QueryExecution/Query.cs | 96 +++++++++++++++++-- .../QueryExecution/QueryExecutionService.cs | 79 ++++++++++++--- .../QueryExecution/ResultSet.cs | 6 +- .../QueryExecution/ExecuteTests.cs | 8 +- 10 files changed, 203 insertions(+), 37 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs index 51e1b5dd..70e6631c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryDisposeRequest.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs index b5c69941..f81edb62 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteCompleteNotification.cs @@ -1,7 +1,15 @@ -using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { + /// + /// Parameters to be sent back with a query execution complete event + /// public class QueryExecuteCompleteParams { /// @@ -17,7 +25,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// /// Whether or not the query was successful. True indicates errors, false indicates success /// - public bool Error { get; set; } + public bool HasError { get; set; } /// /// Summaries of the result sets that were returned with the query diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs index 59453fb9..cac98c1a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteRequest.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs index 8a7b3587..cdf434bb 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/QueryExecuteSubsetRequest.cs @@ -3,7 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts @@ -42,7 +41,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public class QueryExecuteSubsetResult { + /// + /// Subset request error messages. Optional, can be set to null to indicate no errors + /// public string Message { get; set; } + + /// + /// The requested subset of results. Optional, can be set to null to indicate an error + /// public ResultSetSubset ResultSubset { get; set; } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs index a9256581..8e2b49a9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs @@ -1,13 +1,24 @@ -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.QueryExecution.Contracts { + /// + /// Class used to represent a subset of results from a query for transmission across JSON RPC + /// public class ResultSetSubset { + /// + /// The number of rows returned from result set, useful for determining if less rows were + /// returned than requested. + /// public int RowCount { get; set; } + + /// + /// 2D array of the cell values requested from result set + /// public object[][] Rows { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs index 416aafb8..5f8de12a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSummary.cs @@ -1,7 +1,15 @@ -using System.Data.Common; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Data.Common; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts { + /// + /// Represents a summary of information about a result without returning any cells of the results + /// public class ResultSetSummary { /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index eeaa8903..434188a5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.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,18 +15,39 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { - public class Query //: IDisposable + /// + /// Internal representation of an active query + /// + public class Query : IDisposable { #region Properties - public string QueryText { get; set; } - - public ConnectionInfo EditorConnection { get; set; } - + /// + /// Cancellation token source, used for cancelling async db actions + /// private readonly CancellationTokenSource cancellationSource; + /// + /// 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; } + + public bool HasExecuted { get; set; } + + /// + /// The text of the query to execute + /// + public string QueryText { 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 @@ -35,10 +61,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } - public bool HasExecuted { get; set; } - #endregion + /// + /// Constructor for a query + /// + /// The text of the query to execute + /// The information of the connection to use to execute the query public Query(string queryText, ConnectionInfo connection) { // Sanity check for input @@ -59,6 +88,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution cancellationSource = new CancellationTokenSource(); } + /// + /// Executes this query asynchronously and collects all result sets + /// public async Task Execute() { // Sanity check to make sure we haven't already run this query @@ -67,11 +99,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution throw new InvalidOperationException("Query has already executed."); } + DbConnection conn = null; + // Create a connection from the connection details try { string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails); - using (DbConnection conn = EditorConnection.Factory.CreateSqlConnection(connectionString)) + using (EditorConnection.Factory.CreateSqlConnection(connectionString)) { await conn.OpenAsync(cancellationSource.Token); @@ -112,6 +146,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } } + catch (Exception) + { + // Dispose of the connection + conn?.Dispose(); + } finally { // Mark that we have executed @@ -119,6 +158,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// Retrieves a subset of the result sets + /// + /// 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 that the results are available @@ -152,5 +198,37 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution RowCount = rows.Length }; } + + #region IDisposable Implementation + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + cancellationSource.Dispose(); + } + + disposed = true; + } + + ~Query() + { + Dispose(false); + } + + #endregion } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index b98b5ac8..6480e4ba 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -14,7 +14,10 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { - public sealed class QueryExecutionService + /// + /// Service for executing queries + /// + public sealed class QueryExecutionService : IDisposable { #region Singleton Instance Implementation @@ -39,24 +42,32 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution #region Properties - private readonly Lazy> queries = - new Lazy>(() => new ConcurrentDictionary()); - + /// + /// The collection of active queries + /// internal ConcurrentDictionary ActiveQueries { get { return queries.Value; } } + /// + /// Instance of the connection service, used to get the connection info for a given owner URI + /// private ConnectionService ConnectionService { get; set; } + /// + /// Internal storage of active queries, lazily constructed as a threadsafe dictionary + /// + private readonly Lazy> queries = + new Lazy>(() => new ConcurrentDictionary()); + #endregion - #region Public Methods - /// - /// + /// Initializes the service with the service host, registers request handlers and shutdown + /// event handler. /// - /// + /// The service host instance to register with public void InitializeService(ServiceHost serviceHost) { // Register handlers for requests @@ -64,11 +75,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution serviceHost.SetRequestHandler(QueryExecuteSubsetRequest.Type, HandleResultSubsetRequest); serviceHost.SetRequestHandler(QueryDisposeRequest.Type, HandleDisposeRequest); - // Register handlers for events + // Register handler for shutdown event + serviceHost.RegisterShutdownTask((shutdownParams, requestContext) => + { + Dispose(); + return Task.FromResult(0); + }); } - #endregion - #region Request Handlers public async Task HandleExecuteRequest(QueryExecuteParams executeParams, @@ -167,6 +181,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution #endregion + #region Private Helpers + private async Task CreateAndActivateNewQuery(QueryExecuteParams executeParams, RequestContext requestContext) { try @@ -232,7 +248,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution await Task.WhenAll(executeTask); QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams { - Error = false, + HasError = false, Messages = new string[] { }, // TODO: Figure out how to get messages back from the server OwnerUri = executeParams.OwnerUri, ResultSetSummaries = query.ResultSummary @@ -244,7 +260,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Dump the message to a complete event QueryExecuteCompleteParams errorEvent = new QueryExecuteCompleteParams { - Error = true, + HasError = true, Messages = new[] {dbe.Message}, OwnerUri = executeParams.OwnerUri, ResultSetSummaries = query.ResultSummary @@ -252,5 +268,42 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, errorEvent); } } + + #endregion + + #region IDisposable Implementation + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + foreach (var query in ActiveQueries) + { + query.Value.Dispose(); + } + } + + disposed = true; + } + + ~QueryExecutionService() + { + Dispose(false); + } + + #endregion } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs index ee7d7852..fed08ea3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs @@ -1,4 +1,8 @@ -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.Collections.Generic; using System.Data.Common; diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs index ffc0a870..cddf1831 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/ExecuteTests.cs @@ -215,7 +215,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Null(result.Messages); Assert.Empty(completeParams.Messages); Assert.Empty(completeParams.ResultSetSummaries); - Assert.False(completeParams.Error); + Assert.False(completeParams.HasError); Assert.Equal(1, queryService.ActiveQueries.Count); } @@ -241,7 +241,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution Assert.Null(result.Messages); Assert.Empty(completeParams.Messages); Assert.NotEmpty(completeParams.ResultSetSummaries); - Assert.False(completeParams.Error); + Assert.False(completeParams.HasError); Assert.Equal(1, queryService.ActiveQueries.Count); } @@ -321,7 +321,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... There should only be one active query VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Never()); Assert.Null(result.Messages); - Assert.False(complete.Error); + Assert.False(complete.HasError); Assert.Equal(1, queryService.ActiveQueries.Count); } @@ -368,7 +368,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution // ... A completion event should have been sent with error VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Never()); Assert.Null(result.Messages); - Assert.True(complete.Error); + Assert.True(complete.HasError); Assert.NotEmpty(complete.Messages); }