mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-26 01:25:42 -05:00
Merge pull request #15 from Microsoft/feature/queryExecutionV1
Query Execution v1
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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<TResult>(
|
||||
resultDetails,
|
||||
@@ -28,14 +30,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
requestMessage.Id);
|
||||
}
|
||||
|
||||
public async Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
|
||||
public virtual async Task SendEvent<TParams>(EventType<TParams> 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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the query dispose request
|
||||
/// </summary>
|
||||
public class QueryDisposeParams
|
||||
{
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return as the result of a query dispose request
|
||||
/// </summary>
|
||||
public class QueryDisposeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Any error messages that occurred during disposing the result set. Optional, can be set
|
||||
/// to null if there were no errors.
|
||||
/// </summary>
|
||||
public string Messages { get; set; }
|
||||
}
|
||||
|
||||
public class QueryDisposeRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryDisposeParams, QueryDisposeResult> Type =
|
||||
RequestType<QueryDisposeParams, QueryDisposeResult>.Create("query/dispose");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters to be sent back with a query execution complete event
|
||||
/// </summary>
|
||||
public class QueryExecuteCompleteParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the editor that owns the query
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any messages that came back from the server during execution of the query
|
||||
/// </summary>
|
||||
public string[] Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the query was successful. True indicates errors, false indicates success
|
||||
/// </summary>
|
||||
public bool HasError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Summaries of the result sets that were returned with the query
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSetSummaries { get; set; }
|
||||
}
|
||||
|
||||
public class QueryExecuteCompleteEvent
|
||||
{
|
||||
public static readonly
|
||||
EventType<QueryExecuteCompleteParams> Type =
|
||||
EventType<QueryExecuteCompleteParams>.Create("query/complete");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the query execute request
|
||||
/// </summary>
|
||||
public class QueryExecuteParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The text of the query to execute
|
||||
/// </summary>
|
||||
public string QueryText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URI for the editor that is asking for the query execute
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the query execute result
|
||||
/// </summary>
|
||||
public class QueryExecuteResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Connection error messages. Optional, can be set to null to indicate no errors
|
||||
/// </summary>
|
||||
public string Messages { get; set; }
|
||||
}
|
||||
|
||||
public class QueryExecuteRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryExecuteParams, QueryExecuteResult> Type =
|
||||
RequestType<QueryExecuteParams, QueryExecuteResult>.Create("query/execute");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for a query result subset retrieval request
|
||||
/// </summary>
|
||||
public class QueryExecuteSubsetParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI for the file that owns the query to look up the results for
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Index of the result set to get the results from
|
||||
/// </summary>
|
||||
public int ResultSetIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Beginning index of the rows to return from the selected resultset. This index will be
|
||||
/// included in the results.
|
||||
/// </summary>
|
||||
public int RowsStartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public int RowsCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for the result of a subset retrieval request
|
||||
/// </summary>
|
||||
public class QueryExecuteSubsetResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Subset request error messages. Optional, can be set to null to indicate no errors
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The requested subset of results. Optional, can be set to null to indicate an error
|
||||
/// </summary>
|
||||
public ResultSetSubset ResultSubset { get; set; }
|
||||
}
|
||||
|
||||
public class QueryExecuteSubsetRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<QueryExecuteSubsetParams, QueryExecuteSubsetResult> Type =
|
||||
RequestType<QueryExecuteSubsetParams, QueryExecuteSubsetResult>.Create("query/subset");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Class used to represent a subset of results from a query for transmission across JSON RPC
|
||||
/// </summary>
|
||||
public class ResultSetSubset
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of rows returned from result set, useful for determining if less rows were
|
||||
/// returned than requested.
|
||||
/// </summary>
|
||||
public int RowCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 2D array of the cell values requested from result set
|
||||
/// </summary>
|
||||
public object[][] Rows { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a summary of information about a result without returning any cells of the results
|
||||
/// </summary>
|
||||
public class ResultSetSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the result set within the query results
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows that was returned with the resultset
|
||||
/// </summary>
|
||||
public int RowCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Details about the columns that are provided as solutions
|
||||
/// </summary>
|
||||
public DbColumn[] ColumnInfo { get; set; }
|
||||
}
|
||||
}
|
||||
234
src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs
Normal file
234
src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
//
|
||||
// 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;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal representation of an active query
|
||||
/// </summary>
|
||||
public class Query : IDisposable
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token source, used for cancelling async db actions
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource cancellationSource;
|
||||
|
||||
/// <summary>
|
||||
/// The connection info associated with the file editor owner URI, used to create a new
|
||||
/// connection upon execution of the query
|
||||
/// </summary>
|
||||
public ConnectionInfo EditorConnection { get; set; }
|
||||
|
||||
public bool HasExecuted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text of the query to execute
|
||||
/// </summary>
|
||||
public string QueryText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The result sets of the query execution
|
||||
/// </summary>
|
||||
public List<ResultSet> ResultSets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Property for generating a set result set summaries from the result sets
|
||||
/// </summary>
|
||||
public ResultSetSummary[] ResultSummary
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResultSets.Select((set, index) => new ResultSetSummary
|
||||
{
|
||||
ColumnInfo = set.Columns,
|
||||
Id = index,
|
||||
RowCount = set.Rows.Count
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for a query
|
||||
/// </summary>
|
||||
/// <param name="queryText">The text of the query to execute</param>
|
||||
/// <param name="connection">The information of the connection to use to execute the query</param>
|
||||
public Query(string queryText, ConnectionInfo connection)
|
||||
{
|
||||
// Sanity check for input
|
||||
if (String.IsNullOrWhiteSpace(queryText))
|
||||
{
|
||||
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<ResultSet>();
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes this query asynchronously and collects all result sets
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
|
||||
DbConnection conn = null;
|
||||
|
||||
// Create a connection from the connection details
|
||||
try
|
||||
{
|
||||
string connectionString = ConnectionService.BuildConnectionString(EditorConnection.ConnectionDetails);
|
||||
using (EditorConnection.Factory.CreateSqlConnection(connectionString))
|
||||
{
|
||||
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
|
||||
{
|
||||
// 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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Dispose of the connection
|
||||
conn?.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Mark that we have executed
|
||||
HasExecuted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a subset of the result sets
|
||||
/// </summary>
|
||||
/// <param name="resultSetIndex">The index for selecting the result set</param>
|
||||
/// <param name="startRow">The starting row of the results</param>
|
||||
/// <param name="rowCount">How many rows to retrieve</param>
|
||||
/// <returns>A subset of results</returns>
|
||||
public 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
|
||||
};
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
//
|
||||
// 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;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for executing queries
|
||||
/// </summary>
|
||||
public sealed class QueryExecutionService : IDisposable
|
||||
{
|
||||
#region Singleton Instance Implementation
|
||||
|
||||
private static readonly Lazy<QueryExecutionService> instance = new Lazy<QueryExecutionService>(() => new QueryExecutionService());
|
||||
|
||||
public static QueryExecutionService Instance
|
||||
{
|
||||
get { return instance.Value; }
|
||||
}
|
||||
|
||||
private QueryExecutionService()
|
||||
{
|
||||
ConnectionService = ConnectionService.Instance;
|
||||
}
|
||||
|
||||
internal QueryExecutionService(ConnectionService connService)
|
||||
{
|
||||
ConnectionService = connService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The collection of active queries
|
||||
/// </summary>
|
||||
internal ConcurrentDictionary<string, Query> ActiveQueries
|
||||
{
|
||||
get { return queries.Value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instance of the connection service, used to get the connection info for a given owner URI
|
||||
/// </summary>
|
||||
private ConnectionService ConnectionService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Internal storage of active queries, lazily constructed as a threadsafe dictionary
|
||||
/// </summary>
|
||||
private readonly Lazy<ConcurrentDictionary<string, Query>> queries =
|
||||
new Lazy<ConcurrentDictionary<string, Query>>(() => new ConcurrentDictionary<string, Query>());
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the service with the service host, registers request handlers and shutdown
|
||||
/// event handler.
|
||||
/// </summary>
|
||||
/// <param name="serviceHost">The service host instance to register with</param>
|
||||
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 handler for shutdown event
|
||||
serviceHost.RegisterShutdownTask((shutdownParams, requestContext) =>
|
||||
{
|
||||
Dispose();
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
}
|
||||
|
||||
#region Request Handlers
|
||||
|
||||
public async Task HandleExecuteRequest(QueryExecuteParams executeParams,
|
||||
RequestContext<QueryExecuteResult> requestContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get a query new active query
|
||||
Query newQuery = await CreateAndActivateNewQuery(executeParams, requestContext);
|
||||
|
||||
// Execute the query
|
||||
await ExecuteAndCompleteQuery(executeParams, requestContext, newQuery);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Dump any unexpected exceptions as errors
|
||||
await requestContext.SendError(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleResultSubsetRequest(QueryExecuteSubsetParams subsetParams,
|
||||
RequestContext<QueryExecuteSubsetResult> requestContext)
|
||||
{
|
||||
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
|
||||
{
|
||||
Message = null,
|
||||
ResultSubset = query.GetSubset(
|
||||
subsetParams.ResultSetIndex, subsetParams.RowsStartIndex, subsetParams.RowsCount)
|
||||
};
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch (InvalidOperationException ioe)
|
||||
{
|
||||
// Return the error as a result
|
||||
await requestContext.SendResult(new QueryExecuteSubsetResult
|
||||
{
|
||||
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<QueryDisposeResult> requestContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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
|
||||
{
|
||||
Messages = null
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await requestContext.SendError(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
private async Task<Query> CreateAndActivateNewQuery(QueryExecuteParams executeParams, RequestContext<QueryExecuteResult> 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<QueryExecuteResult> 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
|
||||
{
|
||||
HasError = false,
|
||||
Messages = new string[] { }, // TODO: Figure out how to get messages back from the server
|
||||
OwnerUri = executeParams.OwnerUri,
|
||||
ResultSetSummaries = query.ResultSummary
|
||||
};
|
||||
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams);
|
||||
}
|
||||
catch (DbException dbe)
|
||||
{
|
||||
// Dump the message to a complete event
|
||||
QueryExecuteCompleteParams errorEvent = new QueryExecuteCompleteParams
|
||||
{
|
||||
HasError = true,
|
||||
Messages = new[] {dbe.Message},
|
||||
OwnerUri = executeParams.OwnerUri,
|
||||
ResultSetSummaries = query.ResultSummary
|
||||
};
|
||||
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, errorEvent);
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
@@ -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.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
public class ResultSet
|
||||
{
|
||||
public DbColumn[] Columns { get; set; }
|
||||
|
||||
public List<object[]> Rows { get; private set; }
|
||||
|
||||
public ResultSet()
|
||||
{
|
||||
Rows = new List<object[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a row of data to the result set using a <see cref="DbDataReader"/> that has already
|
||||
/// read in a row.
|
||||
/// </summary>
|
||||
/// <param name="reader">A <see cref="DbDataReader"/> that has already had a read performed</param>
|
||||
public void AddRow(DbDataReader reader)
|
||||
{
|
||||
List<object> row = new List<object>();
|
||||
for (int i = 0; i < reader.FieldCount; ++i)
|
||||
{
|
||||
row.Add(reader.GetValue(i));
|
||||
}
|
||||
Rows.Add(row.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
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;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
|
||||
{
|
||||
public class Common
|
||||
{
|
||||
public const string OwnerUri = "testFile";
|
||||
|
||||
public static readonly Dictionary<string, string>[] StandardTestData =
|
||||
{
|
||||
new Dictionary<string, string> { {"col1", "val11"}, { "col2", "val12"}, { "col3", "val13"}, { "col4", "col14"} },
|
||||
new Dictionary<string, string> { {"col1", "val21"}, { "col2", "val22"}, { "col3", "val23"}, { "col4", "col24"} },
|
||||
new Dictionary<string, string> { {"col1", "val31"}, { "col2", "val32"}, { "col3", "val33"}, { "col4", "col34"} },
|
||||
new Dictionary<string, string> { {"col1", "val41"}, { "col2", "val42"}, { "col3", "val43"}, { "col4", "col44"} },
|
||||
new Dictionary<string, string> { {"col1", "val51"}, { "col2", "val52"}, { "col3", "val53"}, { "col4", "col54"} },
|
||||
};
|
||||
|
||||
public static Dictionary<string, string>[] GetTestData(int columns, int rows)
|
||||
{
|
||||
Dictionary<string, string>[] output = new Dictionary<string, string>[rows];
|
||||
for (int row = 0; row < rows; row++)
|
||||
{
|
||||
Dictionary<string, string> rowDictionary = new Dictionary<string, string>();
|
||||
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 DbConnection Mocking
|
||||
|
||||
public static DbCommand CreateTestCommand(Dictionary<string, string>[][] data, bool throwOnRead)
|
||||
{
|
||||
var commandMock = new Mock<DbCommand> { CallBase = true };
|
||||
var commandMockSetup = commandMock.Protected()
|
||||
.Setup<DbDataReader>("ExecuteDbDataReader", It.IsAny<CommandBehavior>());
|
||||
|
||||
// Setup the expected behavior
|
||||
if (throwOnRead)
|
||||
{
|
||||
commandMockSetup.Throws(new Mock<DbException>().Object);
|
||||
}
|
||||
else
|
||||
{
|
||||
commandMockSetup.Returns(new TestDbDataReader(data));
|
||||
}
|
||||
|
||||
|
||||
return commandMock.Object;
|
||||
}
|
||||
|
||||
public static DbConnection CreateTestConnection(Dictionary<string, string>[][] data, bool throwOnRead)
|
||||
{
|
||||
var connectionMock = new Mock<DbConnection> { CallBase = true };
|
||||
connectionMock.Protected()
|
||||
.Setup<DbCommand>("CreateDbCommand")
|
||||
.Returns(CreateTestCommand(data, throwOnRead));
|
||||
|
||||
return connectionMock.Object;
|
||||
}
|
||||
|
||||
public static ISqlConnectionFactory CreateMockFactory(Dictionary<string, string>[][] data, bool throwOnRead)
|
||||
{
|
||||
var mockFactory = new Mock<ISqlConnectionFactory>();
|
||||
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
|
||||
.Returns(CreateTestConnection(data, throwOnRead));
|
||||
|
||||
return mockFactory.Object;
|
||||
}
|
||||
|
||||
public static ConnectionInfo CreateTestConnectionInfo(Dictionary<string, string>[][] 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
|
||||
|
||||
#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<RequestContext<QueryExecuteResult>> GetQueryExecuteResultContextMock(
|
||||
Action<QueryExecuteResult> resultCallback,
|
||||
Action<EventType<QueryExecuteCompleteParams>, QueryExecuteCompleteParams> eventCallback,
|
||||
Action<object> errorCallback)
|
||||
{
|
||||
var requestContext = new Mock<RequestContext<QueryExecuteResult>>();
|
||||
|
||||
// Setup the mock for SendResult
|
||||
var sendResultFlow = requestContext
|
||||
.Setup(rc => rc.SendResult(It.IsAny<QueryExecuteResult>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (resultCallback != null)
|
||||
{
|
||||
sendResultFlow.Callback(resultCallback);
|
||||
}
|
||||
|
||||
// Setup the mock for SendEvent
|
||||
var sendEventFlow = requestContext.Setup(rc => rc.SendEvent(
|
||||
It.Is<EventType<QueryExecuteCompleteParams>>(m => m == QueryExecuteCompleteEvent.Type),
|
||||
It.IsAny<QueryExecuteCompleteParams>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (eventCallback != null)
|
||||
{
|
||||
sendEventFlow.Callback(eventCallback);
|
||||
}
|
||||
|
||||
// Setup the mock for SendError
|
||||
var sendErrorFlow = requestContext.Setup(rc => rc.SendError(It.IsAny<object>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (errorCallback != null)
|
||||
{
|
||||
sendErrorFlow.Callback(errorCallback);
|
||||
}
|
||||
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
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<RequestContext<QueryDisposeResult>> GetQueryDisposeResultContextMock(
|
||||
Action<QueryDisposeResult> resultCallback,
|
||||
Action<object> errorCallback)
|
||||
{
|
||||
var requestContext = new Mock<RequestContext<QueryDisposeResult>>();
|
||||
|
||||
// Setup the mock for SendResult
|
||||
var sendResultFlow = requestContext
|
||||
.Setup(rc => rc.SendResult(It.IsAny<QueryDisposeResult>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (resultCallback != null)
|
||||
{
|
||||
sendResultFlow.Callback(resultCallback);
|
||||
}
|
||||
|
||||
// Setup the mock for SendError
|
||||
var sendErrorFlow = requestContext
|
||||
.Setup(rc => rc.SendError(It.IsAny<object>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (errorCallback != null)
|
||||
{
|
||||
sendErrorFlow.Callback(errorCallback);
|
||||
}
|
||||
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
private void VerifyQueryDisposeCallCount(Mock<RequestContext<QueryDisposeResult>> mock, Times sendResultCalls,
|
||||
Times sendErrorCalls)
|
||||
{
|
||||
mock.Verify(rc => rc.SendResult(It.IsAny<QueryDisposeResult>()), sendResultCalls);
|
||||
mock.Verify(rc => rc.SendError(It.IsAny<object>()), sendErrorCalls);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
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()
|
||||
{
|
||||
// If I create a new query...
|
||||
Query query = new Query("NO OP", Common.CreateTestConnectionInfo(null, false));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryExecuteNoResultSets()
|
||||
{
|
||||
// If I execute a query that should get no result sets
|
||||
Query query = new Query("Query with no result sets", Common.CreateTestConnectionInfo(null, false));
|
||||
query.Execute().Wait();
|
||||
|
||||
// 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 = Common.CreateTestConnectionInfo(new[] {Common.StandardTestData}, false);
|
||||
|
||||
// If I execute a query that should get one result set
|
||||
int resultSets = 1;
|
||||
int rows = 5;
|
||||
int columns = 4;
|
||||
Query query = new Query("Query with one result sets", ci);
|
||||
query.Execute().Wait();
|
||||
|
||||
// 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[] {Common.StandardTestData, Common.StandardTestData};
|
||||
int resultSets = dataset.Length;
|
||||
int rows = Common.StandardTestData.Length;
|
||||
int columns = Common.StandardTestData[0].Count;
|
||||
ConnectionInfo ci = Common.CreateTestConnectionInfo(dataset, false);
|
||||
|
||||
// If I execute a query that should get two result sets
|
||||
Query query = new Query("Query with two result sets", ci);
|
||||
query.Execute().Wait();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryExecuteInvalidQuery()
|
||||
{
|
||||
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true);
|
||||
|
||||
// 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<AggregateException>(() => query.Execute().Wait());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryExecuteExecutedQuery()
|
||||
{
|
||||
ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] {Common.StandardTestData}, false);
|
||||
|
||||
// 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<AggregateException>(() => query.Execute().Wait());
|
||||
Assert.Equal(1, ae.InnerExceptions.Count);
|
||||
Assert.IsType<InvalidOperationException>(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);
|
||||
}
|
||||
|
||||
[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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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.HasError);
|
||||
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.HasError);
|
||||
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.HasError);
|
||||
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.HasError);
|
||||
Assert.NotEmpty(complete.Messages);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void VerifyQueryExecuteCallCount(Mock<RequestContext<QueryExecuteResult>> mock, Times sendResultCalls, Times sendEventCalls, Times sendErrorCalls)
|
||||
{
|
||||
mock.Verify(rc => rc.SendResult(It.IsAny<QueryExecuteResult>()), sendResultCalls);
|
||||
mock.Verify(rc => rc.SendEvent(
|
||||
It.Is<EventType<QueryExecuteCompleteParams>>(m => m == QueryExecuteCompleteEvent.Type),
|
||||
It.IsAny<QueryExecuteCompleteParams>()), sendEventCalls);
|
||||
mock.Verify(rc => rc.SendError(It.IsAny<object>()), sendErrorCalls);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
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)]
|
||||
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<InvalidOperationException>(() => 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<ArgumentOutOfRangeException>(() => 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<RequestContext<QueryExecuteSubsetResult>> GetQuerySubsetResultContextMock(
|
||||
Action<QueryExecuteSubsetResult> resultCallback,
|
||||
Action<object> errorCallback)
|
||||
{
|
||||
var requestContext = new Mock<RequestContext<QueryExecuteSubsetResult>>();
|
||||
|
||||
// Setup the mock for SendResult
|
||||
var sendResultFlow = requestContext
|
||||
.Setup(rc => rc.SendResult(It.IsAny<QueryExecuteSubsetResult>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (resultCallback != null)
|
||||
{
|
||||
sendResultFlow.Callback(resultCallback);
|
||||
}
|
||||
|
||||
// Setup the mock for SendError
|
||||
var sendErrorFlow = requestContext
|
||||
.Setup(rc => rc.SendError(It.IsAny<object>()))
|
||||
.Returns(Task.FromResult(0));
|
||||
if (errorCallback != null)
|
||||
{
|
||||
sendErrorFlow.Callback(errorCallback);
|
||||
}
|
||||
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
private void VerifyQuerySubsetCallCount(Mock<RequestContext<QueryExecuteSubsetResult>> mock, Times sendResultCalls,
|
||||
Times sendErrorCalls)
|
||||
{
|
||||
mock.Verify(rc => rc.SendResult(It.IsAny<QueryExecuteSubsetResult>()), sendResultCalls);
|
||||
mock.Verify(rc => rc.SendError(It.IsAny<object>()), sendErrorCalls);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// 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<string, string>[][] Data { get; set; }
|
||||
|
||||
public IEnumerator<Dictionary<string, string>[]> ResultSet { get; private set; }
|
||||
|
||||
private IEnumerator<Dictionary<string, string>> Rows { get; set; }
|
||||
|
||||
public TestDbDataReader(Dictionary<string, string>[][] data)
|
||||
{
|
||||
Data = data;
|
||||
if (Data != null)
|
||||
{
|
||||
ResultSet = ((IEnumerable<Dictionary<string, string>[]>) 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<Dictionary<string, string>>) ResultSet.Current).GetEnumerator();
|
||||
}
|
||||
return Rows.MoveNext();
|
||||
}
|
||||
|
||||
public override bool NextResult()
|
||||
{
|
||||
if (Data == null || !ResultSet.MoveNext())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Rows = ((IEnumerable<Dictionary<string, string>>)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<DbColumn> GetColumnSchema()
|
||||
{
|
||||
if (ResultSet?.Current == null || ResultSet.Current.Length <= 0)
|
||||
{
|
||||
return new ReadOnlyCollection<DbColumn>(new List<DbColumn>());
|
||||
}
|
||||
|
||||
List<DbColumn> columns = new List<DbColumn>();
|
||||
for (int i = 0; i < ResultSet.Current[0].Count; i++)
|
||||
{
|
||||
columns.Add(new Mock<DbColumn>().Object);
|
||||
}
|
||||
return new ReadOnlyCollection<DbColumn>(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
|
||||
}
|
||||
}
|
||||
@@ -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<Dictionary<string, string>> tableNamesTest = new List<Dictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string> { {"name", "table1"} },
|
||||
new Dictionary<string, string> { {"name", "table2"} }
|
||||
};
|
||||
|
||||
private IEnumerator<Dictionary<string, string>> 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<Dictionary<string, string>>)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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test mock class for IDbCommand
|
||||
/// </summary>
|
||||
public class TestSqlCommand : DbCommand
|
||||
{
|
||||
internal TestSqlCommand(Dictionary<string, string>[][] data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
internal Dictionary<string, string>[][] 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
|
||||
/// </summary>
|
||||
public class TestSqlConnection : DbConnection
|
||||
{
|
||||
internal TestSqlConnection(Dictionary<string, string>[][] data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
internal Dictionary<string, string>[][] 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
|
||||
};
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"moq.netcore": "4.4.0-beta8",
|
||||
"Microsoft.SqlTools.ServiceLayer": {
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.TraceSource": "4.0.0"
|
||||
},
|
||||
"testRunner": "xunit",
|
||||
"frameworks": {
|
||||
|
||||
Reference in New Issue
Block a user