Progressive Results Part 2: Result Completion Event (#134)

The main change in this pull request is to add a new event that will be fired upon completion of a resultset but before the completion of a batch. This event will only fire if a resultset is available and generated.

Changes:
* ConnectionService - Slight changes to enable mocking, cleanup 
* Batch - Moving summary generation into ResultSet class, adding generation of ordinals for resultset and locking of result set list (which needs further refinement, but would be outside scope of this change)
* Adding new event and associated parameters for completion of a resultset. Params return the resultset summary
* Adding logic for assigning the event a handler in the query execution service
* Adding unit tests for testing the new event /making sure the existing tests work
* Refactoring some private properties into member variables

* Refactor to remove SectionData class in favor of BufferRange

* Adding callback for batch completion that will let the extension know that a batch has completed execution

* Refactoring to make progressive results work as per async query execution

* Allowing retrieval of batch results while query is in progress

* reverting global.json, whoops

* Adding a few missing comments, and fixing a couple code style bugs

* Using SelectionData everywhere again

* One more missing comment

* Adding new notification type for result set completion

* Plumbing event for result set completion

* Unit tests for result set events

This includes a fairly substantial change to create a mock of the
ConnectionService and to create separate memorystream storage arrays. It
preserves more correct behavior with a integration test, fixes an issue
where the test db reader will return n-1 rows because the Reliable
Connection Helper steals a record.

* Adding locking to ResultSets for thread safety

* Adding/fixing unit tests

* Adding batch ID to result set summary
This commit is contained in:
Benjamin Russell
2016-11-22 17:37:27 -08:00
committed by GitHub
parent 0841ad7cf4
commit d9efb95386
17 changed files with 585 additions and 190 deletions

View File

@@ -29,7 +29,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
/// <summary> /// <summary>
/// Singleton service instance /// Singleton service instance
/// </summary> /// </summary>
private static Lazy<ConnectionService> instance private static readonly Lazy<ConnectionService> instance
= new Lazy<ConnectionService>(() => new ConnectionService()); = new Lazy<ConnectionService>(() => new ConnectionService());
/// <summary> /// <summary>
@@ -48,11 +48,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
/// </summary> /// </summary>
private ISqlConnectionFactory connectionFactory; private ISqlConnectionFactory connectionFactory;
private Dictionary<string, ConnectionInfo> ownerToConnectionMap = new Dictionary<string, ConnectionInfo>(); private readonly Dictionary<string, ConnectionInfo> ownerToConnectionMap = new Dictionary<string, ConnectionInfo>();
private ConcurrentDictionary<string, CancellationTokenSource> ownerToCancellationTokenSourceMap = new ConcurrentDictionary<string, CancellationTokenSource>(); private readonly ConcurrentDictionary<string, CancellationTokenSource> ownerToCancellationTokenSourceMap = new ConcurrentDictionary<string, CancellationTokenSource>();
private Object cancellationTokenSourceLock = new Object(); private readonly object cancellationTokenSourceLock = new object();
/// <summary> /// <summary>
/// Map from script URIs to ConnectionInfo objects /// Map from script URIs to ConnectionInfo objects
@@ -173,13 +173,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection); connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection);
// try to connect // try to connect
var response = new ConnectionCompleteParams(); var response = new ConnectionCompleteParams {OwnerUri = connectionParams.OwnerUri};
response.OwnerUri = connectionParams.OwnerUri;
CancellationTokenSource source = null; CancellationTokenSource source = null;
try try
{ {
// build the connection string from the input parameters // build the connection string from the input parameters
string connectionString = ConnectionService.BuildConnectionString(connectionInfo.ConnectionDetails); string connectionString = BuildConnectionString(connectionInfo.ConnectionDetails);
// create a sql connection instance // create a sql connection instance
connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString); connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString);
@@ -261,7 +260,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
// Update with the actual database name in connectionInfo and result // Update with the actual database name in connectionInfo and result
// Doing this here as we know the connection is open - expect to do this only on connecting // Doing this here as we know the connection is open - expect to do this only on connecting
connectionInfo.ConnectionDetails.DatabaseName = connectionInfo.SqlConnection.Database; connectionInfo.ConnectionDetails.DatabaseName = connectionInfo.SqlConnection.Database;
response.ConnectionSummary = new ConnectionSummary() response.ConnectionSummary = new ConnectionSummary
{ {
ServerName = connectionInfo.ConnectionDetails.ServerName, ServerName = connectionInfo.ConnectionDetails.ServerName,
DatabaseName = connectionInfo.ConnectionDetails.DatabaseName, DatabaseName = connectionInfo.ConnectionDetails.DatabaseName,
@@ -269,7 +268,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
}; };
// invoke callback notifications // invoke callback notifications
invokeOnConnectionActivities(connectionInfo); InvokeOnConnectionActivities(connectionInfo);
// try to get information about the connected SQL Server instance // try to get information about the connected SQL Server instance
try try
@@ -278,7 +277,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
DbConnection connection = reliableConnection != null ? reliableConnection.GetUnderlyingConnection() : connectionInfo.SqlConnection; DbConnection connection = reliableConnection != null ? reliableConnection.GetUnderlyingConnection() : connectionInfo.SqlConnection;
ReliableConnectionHelper.ServerInfo serverInfo = ReliableConnectionHelper.GetServerVersion(connection); ReliableConnectionHelper.ServerInfo serverInfo = ReliableConnectionHelper.GetServerVersion(connection);
response.ServerInfo = new Contracts.ServerInfo() response.ServerInfo = new ServerInfo
{ {
ServerMajorVersion = serverInfo.ServerMajorVersion, ServerMajorVersion = serverInfo.ServerMajorVersion,
ServerMinorVersion = serverInfo.ServerMinorVersion, ServerMinorVersion = serverInfo.ServerMinorVersion,
@@ -399,7 +398,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
connection.Open(); connection.Open();
List<string> results = new List<string>(); List<string> results = new List<string>();
var systemDatabases = new string[] {"master", "model", "msdb", "tempdb"}; var systemDatabases = new[] {"master", "model", "msdb", "tempdb"};
using (DbCommand command = connection.CreateCommand()) using (DbCommand command = connection.CreateCommand())
{ {
command.CommandText = "SELECT name FROM sys.databases ORDER BY name ASC"; command.CommandText = "SELECT name FROM sys.databases ORDER BY name ASC";
@@ -473,7 +472,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
try try
{ {
RunConnectRequestHandlerTask(connectParams, requestContext); RunConnectRequestHandlerTask(connectParams);
await requestContext.SendResult(true); await requestContext.SendResult(true);
} }
catch catch
@@ -482,7 +481,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
} }
} }
private void RunConnectRequestHandlerTask(ConnectParams connectParams, RequestContext<bool> requestContext) private void RunConnectRequestHandlerTask(ConnectParams connectParams)
{ {
// create a task to connect asynchronously so that other requests are not blocked in the meantime // create a task to connect asynchronously so that other requests are not blocked in the meantime
Task.Run(async () => Task.Run(async () =>
@@ -490,7 +489,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
try try
{ {
// open connection based on request details // open connection based on request details
ConnectionCompleteParams result = await ConnectionService.Instance.Connect(connectParams); ConnectionCompleteParams result = await Instance.Connect(connectParams);
await ServiceHost.SendEvent(ConnectionCompleteNotification.Type, result); await ServiceHost.SendEvent(ConnectionCompleteNotification.Type, result);
} }
catch (Exception ex) catch (Exception ex)
@@ -515,7 +514,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
try try
{ {
bool result = ConnectionService.Instance.CancelConnect(cancelParams); bool result = Instance.CancelConnect(cancelParams);
await requestContext.SendResult(result); await requestContext.SendResult(result);
} }
catch(Exception ex) catch(Exception ex)
@@ -535,7 +534,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
try try
{ {
bool result = ConnectionService.Instance.Disconnect(disconnectParams); bool result = Instance.Disconnect(disconnectParams);
await requestContext.SendResult(result); await requestContext.SendResult(result);
} }
catch(Exception ex) catch(Exception ex)
@@ -556,7 +555,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
try try
{ {
ListDatabasesResponse result = ConnectionService.Instance.ListDatabases(listDatabasesParams); ListDatabasesResponse result = Instance.ListDatabases(listDatabasesParams);
await requestContext.SendResult(result); await requestContext.SendResult(result);
} }
catch(Exception ex) catch(Exception ex)
@@ -579,10 +578,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
/// <param name="connectionDetails"></param> /// <param name="connectionDetails"></param>
public static string BuildConnectionString(ConnectionDetails connectionDetails) public static string BuildConnectionString(ConnectionDetails connectionDetails)
{ {
SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder
connectionBuilder["Data Source"] = connectionDetails.ServerName; {
connectionBuilder["User Id"] = connectionDetails.UserName; ["Data Source"] = connectionDetails.ServerName,
connectionBuilder["Password"] = connectionDetails.Password; ["User Id"] = connectionDetails.UserName,
["Password"] = connectionDetails.Password
};
// Check for any optional parameters // Check for any optional parameters
if (!string.IsNullOrEmpty(connectionDetails.DatabaseName)) if (!string.IsNullOrEmpty(connectionDetails.DatabaseName))
@@ -722,7 +723,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
// Fire a connection changed event // Fire a connection changed event
ConnectionChangedParams parameters = new ConnectionChangedParams(); ConnectionChangedParams parameters = new ConnectionChangedParams();
ConnectionSummary summary = (ConnectionSummary)(info.ConnectionDetails); ConnectionSummary summary = info.ConnectionDetails;
parameters.Connection = summary.Clone(); parameters.Connection = summary.Clone();
parameters.OwnerUri = ownerUri; parameters.OwnerUri = ownerUri;
ServiceHost.SendEvent(ConnectionChangedNotification.Type, parameters); ServiceHost.SendEvent(ConnectionChangedNotification.Type, parameters);
@@ -741,7 +742,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
} }
} }
private void invokeOnConnectionActivities(ConnectionInfo connectionInfo) private void InvokeOnConnectionActivities(ConnectionInfo connectionInfo)
{ {
foreach (var activity in this.onConnectionActivities) foreach (var activity in this.onConnectionActivities)
{ {

View File

@@ -88,6 +88,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary> /// </summary>
public event BatchAsyncEventHandler BatchCompletion; public event BatchAsyncEventHandler BatchCompletion;
/// <summary>
/// Event that will be called when the resultset has completed execution. It will not be
/// called from the Batch but from the ResultSet instance
/// </summary>
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompletion;
/// <summary> /// <summary>
/// The text of batch that will be executed /// The text of batch that will be executed
/// </summary> /// </summary>
@@ -155,12 +161,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{ {
get get
{ {
return ResultSets.Select((set, index) => new ResultSetSummary() lock (resultSets)
{ {
ColumnInfo = set.Columns, return resultSets.Select(set => set.Summary).ToArray();
Id = index, }
RowCount = set.RowCount
}).ToArray();
} }
} }
@@ -221,7 +225,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
command = sqlConn.GetUnderlyingConnection().CreateCommand(); command = sqlConn.GetUnderlyingConnection().CreateCommand();
// Add a handler for when the command completes // Add a handler for when the command completes
SqlCommand sqlCommand = (SqlCommand) command; SqlCommand sqlCommand = (SqlCommand)command;
sqlCommand.StatementCompleted += StatementCompletedHandler; sqlCommand.StatementCompleted += StatementCompletedHandler;
} }
else else
@@ -244,6 +248,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// Execute the command to get back a reader // Execute the command to get back a reader
using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken)) using (DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken))
{ {
int resultSetOrdinal = 0;
do do
{ {
// Skip this result set if there aren't any rows (ie, UPDATE/DELETE/etc queries) // Skip this result set if there aren't any rows (ie, UPDATE/DELETE/etc queries)
@@ -253,10 +258,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
} }
// This resultset has results (ie, SELECT/etc queries) // This resultset has results (ie, SELECT/etc queries)
ResultSet resultSet = new ResultSet(reader, outputFileFactory); ResultSet resultSet = new ResultSet(reader, resultSetOrdinal, Id, outputFileFactory);
resultSet.ResultCompletion += ResultSetCompletion;
// Add the result set to the results of the query // Add the result set to the results of the query
resultSets.Add(resultSet); lock (resultSets)
{
resultSets.Add(resultSet);
resultSetOrdinal++;
}
// Read until we hit the end of the result set // Read until we hit the end of the result set
await resultSet.ReadResultToEnd(cancellationToken).ConfigureAwait(false); await resultSet.ReadResultToEnd(cancellationToken).ConfigureAwait(false);
@@ -318,20 +328,21 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// <returns>A subset of results</returns> /// <returns>A subset of results</returns>
public Task<ResultSetSubset> GetSubset(int resultSetIndex, int startRow, int rowCount) public Task<ResultSetSubset> GetSubset(int resultSetIndex, int startRow, int rowCount)
{ {
// Sanity check to make sure that the batch has finished ResultSet targetResultSet;
if (!HasExecuted) lock (resultSets)
{ {
throw new InvalidOperationException(SR.QueryServiceSubsetBatchNotCompleted); // Sanity check to make sure we have valid numbers
} if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count)
{
throw new ArgumentOutOfRangeException(nameof(resultSetIndex),
SR.QueryServiceSubsetResultSetOutOfRange);
}
// Sanity check to make sure we have valid numbers targetResultSet = resultSets[resultSetIndex];
if (resultSetIndex < 0 || resultSetIndex >= resultSets.Count)
{
throw new ArgumentOutOfRangeException(nameof(resultSetIndex), SR.QueryServiceSubsetResultSetOutOfRange);
} }
// Retrieve the result set // Retrieve the result set
return resultSets[resultSetIndex].GetSubset(startRow, rowCount); return targetResultSet.GetSubset(startRow, rowCount);
} }
#endregion #endregion
@@ -431,9 +442,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
if (disposing) if (disposing)
{ {
foreach (ResultSet r in ResultSets) lock (resultSets)
{ {
r.Dispose(); foreach (ResultSet r in resultSets)
{
r.Dispose();
}
} }
} }

View File

@@ -0,0 +1,22 @@
//
// 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
{
public class QueryExecuteResultSetCompleteParams
{
public ResultSetSummary ResultSetSummary { get; set; }
public string OwnerUri { get; set; }
}
public class QueryExecuteResultSetCompleteEvent
{
public static readonly
EventType<QueryExecuteResultSetCompleteParams> Type =
EventType<QueryExecuteResultSetCompleteParams>.Create("query/resultSetComplete");
}
}

View File

@@ -15,6 +15,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// </summary> /// </summary>
public int Id { get; set; } public int Id { get; set; }
/// <summary>
/// The ID of the batch set within the query
/// </summary>
public int BatchId { get; set; }
/// <summary> /// <summary>
/// The number of rows that was returned with the resultset /// The number of rows that was returned with the resultset
/// </summary> /// </summary>

View File

@@ -92,25 +92,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
Batches = batchSelection.ToArray(); Batches = batchSelection.ToArray();
} }
#region Properties #region Events
/// <summary>
/// Delegate type for callback when a query completes or fails
/// </summary>
/// <param name="q">The query that completed</param>
public delegate Task QueryAsyncEventHandler(Query q);
/// <summary> /// <summary>
/// Event to be called when a batch is completed. /// Event to be called when a batch is completed.
/// </summary> /// </summary>
public event Batch.BatchAsyncEventHandler BatchCompleted; public event Batch.BatchAsyncEventHandler BatchCompleted;
/// <summary>
/// Delegate type for callback when a query connection fails
/// </summary>
/// <param name="message">Message to return</param>
public delegate Task QueryAsyncErrorEventHandler(string message);
/// <summary> /// <summary>
/// Callback for when the query has completed successfully /// Callback for when the query has completed successfully
/// </summary> /// </summary>
@@ -126,6 +114,27 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary> /// </summary>
public event QueryAsyncErrorEventHandler QueryConnectionException; public event QueryAsyncErrorEventHandler QueryConnectionException;
/// <summary>
/// Event to be called when a resultset has completed.
/// </summary>
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompleted;
#endregion
#region Properties
/// <summary>
/// Delegate type for callback when a query completes or fails
/// </summary>
/// <param name="q">The query that completed</param>
public delegate Task QueryAsyncEventHandler(Query q);
/// <summary>
/// Delegate type for callback when a query connection fails
/// </summary>
/// <param name="message">Message to return</param>
public delegate Task QueryAsyncErrorEventHandler(string message);
/// <summary> /// <summary>
/// The batches underneath this query /// The batches underneath this query
/// </summary> /// </summary>
@@ -146,6 +155,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
} }
} }
/// <summary>
/// Storage for the async task for execution. Set as internal in order to await completion
/// in unit tests.
/// </summary>
internal Task ExecutionTask { get; private set; } internal Task ExecutionTask { get; private set; }
/// <summary> /// <summary>
@@ -242,7 +255,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{ {
await conn.OpenAsync(); await conn.OpenAsync();
} }
catch(Exception exception) catch (Exception exception)
{ {
this.HasExecuted = true; this.HasExecuted = true;
if (QueryConnectionException != null) if (QueryConnectionException != null)
@@ -265,6 +278,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
foreach (Batch b in Batches) foreach (Batch b in Batches)
{ {
b.BatchCompletion += BatchCompleted; b.BatchCompletion += BatchCompleted;
b.ResultSetCompletion += ResultSetCompleted;
await b.Execute(conn, cancellationSource.Token); await b.Execute(conn, cancellationSource.Token);
} }

View File

@@ -437,7 +437,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}; };
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams); await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams);
}; };
query.QueryCompleted += callback; query.QueryCompleted += callback;
query.QueryFailed += callback; query.QueryFailed += callback;
query.QueryConnectionException += errorCallback; query.QueryConnectionException += errorCallback;
@@ -454,6 +453,18 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}; };
query.BatchCompleted += batchCallback; query.BatchCompleted += batchCallback;
// Setup the ResultSet completion callback
ResultSet.ResultSetAsyncEventHandler resultCallback = async r =>
{
QueryExecuteResultSetCompleteParams eventParams = new QueryExecuteResultSetCompleteParams
{
ResultSetSummary = r.Summary,
OwnerUri = executeParams.OwnerUri
};
await requestContext.SendEvent(QueryExecuteResultSetCompleteEvent.Type, eventParams);
};
query.ResultSetCompleted += resultCallback;
// Launch this as an asynchronous task // Launch this as an asynchronous task
query.Execute(); query.Execute();

View File

@@ -59,9 +59,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
private readonly IFileStreamFactory fileStreamFactory; private readonly IFileStreamFactory fileStreamFactory;
/// <summary> /// <summary>
/// Whether or not the result set has been read in from the database /// Whether or not the result set has been read in from the database,
/// set as internal in order to fake value in unit tests
/// </summary> /// </summary>
private bool hasBeenRead; internal bool hasBeenRead;
/// <summary> /// <summary>
/// Whether resultSet is a 'for xml' or 'for json' result /// Whether resultSet is a 'for xml' or 'for json' result
@@ -74,7 +75,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
private readonly string outputFileName; private readonly string outputFileName;
/// <summary> /// <summary>
/// Whether the resultSet is in the process of being disposed /// All save tasks currently saving this ResultSet
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, Task> saveTasks; private readonly ConcurrentDictionary<string, Task> saveTasks;
@@ -84,13 +85,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// Creates a new result set and initializes its state /// Creates a new result set and initializes its state
/// </summary> /// </summary>
/// <param name="reader">The reader from executing a query</param> /// <param name="reader">The reader from executing a query</param>
/// <param name="ordinal">The ID of the resultset, the ordinal of the result within the batch</param>
/// <param name="batchOrdinal">The ID of the batch, the ordinal of the batch within the query</param>
/// <param name="factory">Factory for creating a reader/writer</param> /// <param name="factory">Factory for creating a reader/writer</param>
public ResultSet(DbDataReader reader, IFileStreamFactory factory) public ResultSet(DbDataReader reader, int ordinal, int batchOrdinal, IFileStreamFactory factory)
{ {
// Sanity check to make sure we got a reader // Sanity check to make sure we got a reader
Validate.IsNotNull(nameof(reader), SR.QueryServiceResultSetReaderNull); Validate.IsNotNull(nameof(reader), SR.QueryServiceResultSetReaderNull);
dataReader = new StorageDataReader(reader); dataReader = new StorageDataReader(reader);
Id = ordinal;
BatchId = batchOrdinal;
// Initialize the storage // Initialize the storage
outputFileName = factory.CreateFile(); outputFileName = factory.CreateFile();
@@ -104,6 +109,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
#region Properties #region Properties
/// <summary>
/// Asynchronous handler for when a resultset has completed
/// </summary>
/// <param name="resultSet">The result set that completed</param>
public delegate Task ResultSetAsyncEventHandler(ResultSet resultSet);
/// <summary>
/// Event that will be called when the result set has completed execution
/// </summary>
public event ResultSetAsyncEventHandler ResultCompletion;
/// <summary> /// <summary>
/// Whether the resultSet is in the process of being disposed /// Whether the resultSet is in the process of being disposed
/// </summary> /// </summary>
@@ -115,6 +131,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary> /// </summary>
public DbColumnWrapper[] Columns { get; private set; } public DbColumnWrapper[] Columns { get; private set; }
/// <summary>
/// ID of the result set, relative to the batch
/// </summary>
public int Id { get; private set; }
/// <summary>
/// ID of the batch set, relative to the query
/// </summary>
public int BatchId { get; private set; }
/// <summary> /// <summary>
/// Maximum number of characters to store for a field /// Maximum number of characters to store for a field
/// </summary> /// </summary>
@@ -130,6 +156,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary> /// </summary>
public long RowCount { get; private set; } public long RowCount { get; private set; }
/// <summary>
/// Generates a summary of this result set
/// </summary>
public ResultSetSummary Summary
{
get
{
return new ResultSetSummary
{
ColumnInfo = Columns,
Id = Id,
BatchId = BatchId,
RowCount = RowCount
};
}
}
#endregion #endregion
#region Public Methods #region Public Methods
@@ -170,9 +213,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
if (isSingleColumnXmlJsonResultSet) if (isSingleColumnXmlJsonResultSet)
{ {
// Iterate over all the rows and process them into a list of string builders // Iterate over all the rows and process them into a list of string builders
// ReSharper disable once AccessToDisposedClosure The lambda is used immediately in string.Join call
IEnumerable<string> rowValues = fileOffsets.Select(rowOffset => fileStreamReader.ReadRow(rowOffset, Columns)[0].DisplayValue); IEnumerable<string> rowValues = fileOffsets.Select(rowOffset => fileStreamReader.ReadRow(rowOffset, Columns)[0].DisplayValue);
rows = new[] { new[] { string.Join(string.Empty, rowValues) } }; rows = new[] { new[] { string.Join(string.Empty, rowValues) } };
} }
else else
{ {
@@ -180,8 +223,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
IEnumerable<long> rowOffsets = fileOffsets.Skip(startRow).Take(rowCount); IEnumerable<long> rowOffsets = fileOffsets.Skip(startRow).Take(rowCount);
// Iterate over the rows we need and process them into output // Iterate over the rows we need and process them into output
rows = rowOffsets.Select(rowOffset => // ReSharper disable once AccessToDisposedClosure The lambda is used immediately in .ToArray call
fileStreamReader.ReadRow(rowOffset, Columns).Select(cell => cell.DisplayValue).ToArray()) rows = rowOffsets.Select(rowOffset => fileStreamReader.ReadRow(rowOffset, Columns)
.Select(cell => cell.DisplayValue).ToArray())
.ToArray(); .ToArray();
} }
@@ -201,33 +245,41 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// <param name="cancellationToken">Cancellation token for cancelling the query</param> /// <param name="cancellationToken">Cancellation token for cancelling the query</param>
public async Task ReadResultToEnd(CancellationToken cancellationToken) public async Task ReadResultToEnd(CancellationToken cancellationToken)
{ {
// Mark that result has been read try
hasBeenRead = true;
// Open a writer for the file
using (IFileStreamWriter fileWriter = fileStreamFactory.GetWriter(outputFileName, MaxCharsToStore, MaxXmlCharsToStore))
{ {
// If we can initialize the columns using the column schema, use that // Mark that result has been read
if (!dataReader.DbDataReader.CanGetColumnSchema()) hasBeenRead = true;
// Open a writer for the file
var fileWriter = fileStreamFactory.GetWriter(outputFileName, MaxCharsToStore, MaxCharsToStore);
using (fileWriter)
{ {
throw new InvalidOperationException(SR.QueryServiceResultSetNoColumnSchema); // If we can initialize the columns using the column schema, use that
if (!dataReader.DbDataReader.CanGetColumnSchema())
{
throw new InvalidOperationException(SR.QueryServiceResultSetNoColumnSchema);
}
Columns = dataReader.Columns;
long currentFileOffset = 0;
while (await dataReader.ReadAsync(cancellationToken))
{
RowCount++;
fileOffsets.Add(currentFileOffset);
currentFileOffset += fileWriter.WriteRow(dataReader);
}
} }
Columns = dataReader.Columns; // Check if resultset is 'for xml/json'. If it is, set isJson/isXml value in column metadata
SingleColumnXmlJsonResultSet();
long currentFileOffset = 0; }
while (await dataReader.ReadAsync(cancellationToken)) finally
{
// Fire off a result set completion event if we have one
if (ResultCompletion != null)
{ {
// Store the beginning of the row await ResultCompletion(this);
long rowStart = currentFileOffset;
currentFileOffset += fileWriter.WriteRow(dataReader);
// Add the row to the list of rows we have only if the row was successfully written
RowCount++;
fileOffsets.Add(rowStart);
} }
} }
// Check if resultset is 'for xml/json'. If it is, set isJson/isXml value in column metadata
SingleColumnXmlJsonResultSet();
} }
#endregion #endregion
@@ -284,7 +336,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// If the result set represented by this class corresponds to a single JSON /// If the result set represented by this class corresponds to a single JSON
/// column that contains results of "for json" query, set isJson = true /// column that contains results of "for json" query, set isJson = true
/// </summary> /// </summary>
private void SingleColumnXmlJsonResultSet() { private void SingleColumnXmlJsonResultSet()
{
if (Columns?.Length == 1 && RowCount != 0) if (Columns?.Length == 1 && RowCount != 0)
{ {

View File

@@ -91,7 +91,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
} }
[Fact] [Fact]
public async void CancelNonExistantTest() public async Task CancelNonExistantTest()
{ {
var workspaceService = new Mock<WorkspaceService<SqlToolsSettings>>(); var workspaceService = new Mock<WorkspaceService<SqlToolsSettings>>();

View File

@@ -32,7 +32,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
public const string NoOpQuery = "-- No ops here, just us chickens."; public const string NoOpQuery = "-- No ops here, just us chickens.";
public const int Ordinal = 0; public const int Ordinal = 100; // We'll pick something other than default(int)
public const string OwnerUri = "testFile"; public const string OwnerUri = "testFile";
@@ -161,7 +161,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
var connectionMock = new Mock<DbConnection> { CallBase = true }; var connectionMock = new Mock<DbConnection> { CallBase = true };
connectionMock.Protected() connectionMock.Protected()
.Setup<DbCommand>("CreateDbCommand") .Setup<DbCommand>("CreateDbCommand")
.Returns(CreateTestCommand(data, throwOnRead)); .Returns(() => CreateTestCommand(data, throwOnRead));
connectionMock.Setup(dbc => dbc.Open()) connectionMock.Setup(dbc => dbc.Open())
.Callback(() => connectionMock.SetupGet(dbc => dbc.State).Returns(ConnectionState.Open)); .Callback(() => connectionMock.SetupGet(dbc => dbc.State).Returns(ConnectionState.Open));
connectionMock.Setup(dbc => dbc.Close()) connectionMock.Setup(dbc => dbc.Close())
@@ -174,7 +174,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
{ {
var mockFactory = new Mock<ISqlConnectionFactory>(); var mockFactory = new Mock<ISqlConnectionFactory>();
mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>())) mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny<string>()))
.Returns(CreateTestConnection(data, throwOnRead)); .Returns(() => CreateTestConnection(data, throwOnRead));
return mockFactory.Object; return mockFactory.Object;
} }

View File

@@ -28,7 +28,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
var mockDataReader = Common.CreateTestConnection(null, false).CreateCommand().ExecuteReaderAsync().Result; var mockDataReader = Common.CreateTestConnection(null, false).CreateCommand().ExecuteReaderAsync().Result;
// If: I setup a single resultset and then dispose it // If: I setup a single resultset and then dispose it
ResultSet rs = new ResultSet(mockDataReader, mockFileStreamFactory.Object); ResultSet rs = new ResultSet(mockDataReader, Common.Ordinal, Common.Ordinal, mockFileStreamFactory.Object);
rs.Dispose(); rs.Dispose();
// Then: The file that was created should have been deleted // Then: The file that was created should have been deleted
@@ -101,6 +101,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
// ... We need a query service // ... We need a query service
var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService.Object); var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService.Object);
// If: // If:
// ... I execute some bogus query // ... I execute some bogus query
var queryParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri }; var queryParams = new QueryExecuteParams { QuerySelection = Common.WholeDocument, OwnerUri = Common.OwnerUri };

View File

@@ -42,6 +42,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... It's ordinal ID should be what I set it to // ... It's ordinal ID should be what I set it to
Assert.Equal(Common.Ordinal, batch.Id); Assert.Equal(Common.Ordinal, batch.Id);
// ... The summary should have the same info
Assert.False(batch.Summary.HasError);
Assert.Equal(Common.Ordinal, batch.Summary.Id);
Assert.Empty(batch.Summary.ResultSetSummaries);
Assert.Empty(batch.Summary.Messages);
Assert.Equal(0, batch.Summary.Selection.StartLine);
Assert.NotEqual(default(DateTime).ToString("o"), batch.Summary.ExecutionStart); // Should have been set at construction
Assert.Equal(default(DateTime).ToString("o"), batch.Summary.ExecutionEnd);
Assert.Equal((default(DateTime) - DateTime.Parse(batch.Summary.ExecutionStart)).ToString(), batch.Summary.ExecutionElapsed);
} }
[Fact] [Fact]
@@ -49,18 +59,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
{ {
// Setup: Create a callback for batch completion // Setup: Create a callback for batch completion
BatchSummary batchSummaryFromCallback = null; BatchSummary batchSummaryFromCallback = null;
bool completionCallbackFired = false; Batch.BatchAsyncEventHandler batchCallback = b =>
Batch.BatchAsyncEventHandler callback = b =>
{ {
completionCallbackFired = true;
batchSummaryFromCallback = b.Summary; batchSummaryFromCallback = b.Summary;
return Task.FromResult(0); return Task.FromResult(0);
}; };
// ... Create a callback for result completion
bool resultCallbackFired = false;
ResultSet.ResultSetAsyncEventHandler resultSetCallback = r =>
{
resultCallbackFired = true;
return Task.FromResult(0);
};
// If I execute a query that should get no result sets // If I execute a query that should get no result sets
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory);
batch.BatchCompletion += callback; batch.BatchCompletion += batchCallback;
batch.ResultSetCompletion += resultSetCallback;
batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None).Wait(); batch.Execute(GetConnection(Common.CreateTestConnectionInfo(null, false)), CancellationToken.None).Wait();
// Then: // Then:
@@ -80,8 +97,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.Equal(1, batch.ResultMessages.Count()); Assert.Equal(1, batch.ResultMessages.Count());
// ... The callback for batch completion should have been fired // ... The callback for batch completion should have been fired
Assert.True(completionCallbackFired);
Assert.NotNull(batchSummaryFromCallback); Assert.NotNull(batchSummaryFromCallback);
// ... The callback for the result set should NOT have been fired
Assert.False(resultCallbackFired);
// ... The summary should have the same info
Assert.False(batch.Summary.HasError);
Assert.Equal(Common.Ordinal, batch.Summary.Id);
Assert.Equal(0, batch.Summary.ResultSetSummaries.Length);
Assert.Equal(1, batch.Summary.Messages.Length);
Assert.Equal(0, batch.Summary.Selection.StartLine);
Assert.True(DateTime.Parse(batch.Summary.ExecutionStart) > default(DateTime));
Assert.True(DateTime.Parse(batch.Summary.ExecutionEnd) > default(DateTime));
Assert.NotNull(batch.Summary.ExecutionElapsed);
} }
[Fact] [Fact]
@@ -92,18 +121,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// Setup: Create a callback for batch completion // Setup: Create a callback for batch completion
BatchSummary batchSummaryFromCallback = null; BatchSummary batchSummaryFromCallback = null;
bool completionCallbackFired = false; Batch.BatchAsyncEventHandler batchCallback = b =>
Batch.BatchAsyncEventHandler callback = b =>
{ {
completionCallbackFired = true;
batchSummaryFromCallback = b.Summary; batchSummaryFromCallback = b.Summary;
return Task.FromResult(0); return Task.FromResult(0);
}; };
// ... Create a callback for result set completion
bool resultCallbackFired = false;
ResultSet.ResultSetAsyncEventHandler resultSetCallback = r =>
{
resultCallbackFired = true;
return Task.FromResult(0);
};
// If I execute a query that should get one result set // If I execute a query that should get one result set
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory);
batch.BatchCompletion += callback; batch.BatchCompletion += batchCallback;
batch.ResultSetCompletion += resultSetCallback;
batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); batch.Execute(GetConnection(ci), CancellationToken.None).Wait();
// Then: // Then:
@@ -112,7 +148,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.False(batch.HasError, "The batch should not have an error"); Assert.False(batch.HasError, "The batch should not have an error");
// ... There should be exactly one result set // ... There should be exactly one result set
Assert.Equal(resultSets, batch.ResultSets.Count()); Assert.Equal(resultSets, batch.ResultSets.Count);
Assert.Equal(resultSets, batch.ResultSummaries.Length); Assert.Equal(resultSets, batch.ResultSummaries.Length);
// ... Inside the result set should be with 5 rows // ... Inside the result set should be with 5 rows
@@ -127,8 +163,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.Equal(resultSets, batch.ResultMessages.Count()); Assert.Equal(resultSets, batch.ResultMessages.Count());
// ... The callback for batch completion should have been fired // ... The callback for batch completion should have been fired
Assert.True(completionCallbackFired);
Assert.NotNull(batchSummaryFromCallback); Assert.NotNull(batchSummaryFromCallback);
// ... The callback for resultset completion should have been fired
Assert.True(resultCallbackFired); // We only want to validate that it happened, validation of the
// summary is done in result set tests
} }
[Fact] [Fact]
@@ -140,18 +179,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// Setup: Create a callback for batch completion // Setup: Create a callback for batch completion
BatchSummary batchSummaryFromCallback = null; BatchSummary batchSummaryFromCallback = null;
bool completionCallbackFired = false; Batch.BatchAsyncEventHandler batchCallback = b =>
Batch.BatchAsyncEventHandler callback = b =>
{ {
completionCallbackFired = true;
batchSummaryFromCallback = b.Summary; batchSummaryFromCallback = b.Summary;
return Task.FromResult(0); return Task.FromResult(0);
}; };
// ... Create a callback for resultset completion
int resultSummaryCount = 0;
ResultSet.ResultSetAsyncEventHandler resultSetCallback = r =>
{
resultSummaryCount++;
return Task.FromResult(0);
};
// If I execute a query that should get two result sets // If I execute a query that should get two result sets
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory);
batch.BatchCompletion += callback; batch.BatchCompletion += batchCallback;
batch.ResultSetCompletion += resultSetCallback;
batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); batch.Execute(GetConnection(ci), CancellationToken.None).Wait();
// Then: // Then:
@@ -184,8 +230,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
} }
// ... The callback for batch completion should have been fired // ... The callback for batch completion should have been fired
Assert.True(completionCallbackFired);
Assert.NotNull(batchSummaryFromCallback); Assert.NotNull(batchSummaryFromCallback);
// ... The callback for result set completion should have been fired
Assert.Equal(2, resultSummaryCount);
} }
[Fact] [Fact]
@@ -193,20 +241,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
{ {
// Setup: Create a callback for batch completion // Setup: Create a callback for batch completion
BatchSummary batchSummaryFromCallback = null; BatchSummary batchSummaryFromCallback = null;
bool completionCallbackFired = false; Batch.BatchAsyncEventHandler batchCallback = b =>
Batch.BatchAsyncEventHandler callback = b =>
{ {
completionCallbackFired = true;
batchSummaryFromCallback = b.Summary; batchSummaryFromCallback = b.Summary;
return Task.FromResult(0); return Task.FromResult(0);
}; };
// ... Create a callback that will fail the test if it's called
ResultSet.ResultSetAsyncEventHandler resultSetCallback = r =>
{
throw new Exception("ResultSet callback was called when it should not have been.");
};
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true);
// If I execute a batch that is invalid // If I execute a batch that is invalid
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory); Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory);
batch.BatchCompletion += callback; batch.BatchCompletion += batchCallback;
batch.ResultSetCompletion += resultSetCallback;
batch.Execute(GetConnection(ci), CancellationToken.None).Wait(); batch.Execute(GetConnection(ci), CancellationToken.None).Wait();
// Then: // Then:
@@ -222,14 +275,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.NotEmpty(batch.ResultMessages); Assert.NotEmpty(batch.ResultMessages);
// ... The callback for batch completion should have been fired // ... The callback for batch completion should have been fired
Assert.True(completionCallbackFired);
Assert.NotNull(batchSummaryFromCallback); Assert.NotNull(batchSummaryFromCallback);
} }
[Fact] [Fact]
public async Task BatchExecuteExecuted() public async Task BatchExecuteExecuted()
{ {
// Setup: Create a callback for batch completion ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false);
// If I execute a batch
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory);
batch.Execute(GetConnection(ci), CancellationToken.None).Wait();
// Then:
// ... It should have executed without error
Assert.True(batch.HasExecuted, "The batch should have been marked executed.");
Assert.False(batch.HasError, "The batch should not have an error");
// Setup for part 2: Create a callback for batch completion
BatchSummary batchSummaryFromCallback = null; BatchSummary batchSummaryFromCallback = null;
bool completionCallbackFired = false; bool completionCallbackFired = false;
Batch.BatchAsyncEventHandler callback = b => Batch.BatchAsyncEventHandler callback = b =>
@@ -239,26 +303,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
return Task.FromResult(0); return Task.FromResult(0);
}; };
ConnectionInfo ci = Common.CreateTestConnectionInfo(new[] { Common.StandardTestData }, false);
// If I execute a batch
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Batch batch = new Batch(Common.StandardQuery, Common.SubsectionDocument, Common.Ordinal, fileStreamFactory);
batch.BatchCompletion += callback;
batch.Execute(GetConnection(ci), CancellationToken.None).Wait();
// Then:
// ... It should have executed without error
Assert.True(batch.HasExecuted, "The batch should have been marked executed.");
Assert.False(batch.HasError, "The batch should not have an error");
// ... The callback for batch completion should have been fired
Assert.True(completionCallbackFired);
Assert.NotNull(batchSummaryFromCallback);
// If I execute it again // If I execute it again
// Then: // Then:
// ... It should throw an invalid operation exception // ... It should throw an invalid operation exception
batch.BatchCompletion += callback;
await Assert.ThrowsAsync<InvalidOperationException>(() => await Assert.ThrowsAsync<InvalidOperationException>(() =>
batch.Execute(GetConnection(ci), CancellationToken.None)); batch.Execute(GetConnection(ci), CancellationToken.None));
@@ -267,6 +315,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.True(batch.HasExecuted, "The batch should still be marked executed."); Assert.True(batch.HasExecuted, "The batch should still be marked executed.");
Assert.NotEmpty(batch.ResultSets); Assert.NotEmpty(batch.ResultSets);
Assert.NotEmpty(batch.ResultSummaries); Assert.NotEmpty(batch.ResultSummaries);
// ... The callback for batch completion should not have been fired for the second run
Assert.False(completionCallbackFired);
Assert.Null(batchSummaryFromCallback);
} }
[Theory] [Theory]

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.SqlContext;
@@ -61,11 +62,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
[Fact] [Fact]
public void QueryExecuteSingleBatch() public void QueryExecuteSingleBatch()
{ {
// Setup:
// ... Create a callback for batch completion
int batchCallbacksReceived = 0;
Batch.BatchAsyncEventHandler batchCallback = summary =>
{
batchCallbacksReceived++;
return Task.CompletedTask;
};
// If: // If:
// ... I create a query from a single batch (without separator) // ... I create a query from a single batch (without separator)
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Query query = new Query(Common.StandardQuery, ci, new QueryExecutionSettings(), fileStreamFactory); Query query = new Query(Common.StandardQuery, ci, new QueryExecutionSettings(), fileStreamFactory);
query.BatchCompleted += batchCallback;
// Then: // Then:
// ... I should get a single batch to execute that hasn't been executed // ... I should get a single batch to execute that hasn't been executed
@@ -85,16 +96,27 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.True(query.HasExecuted); Assert.True(query.HasExecuted);
Assert.NotEmpty(query.BatchSummaries); Assert.NotEmpty(query.BatchSummaries);
Assert.Equal(1, query.BatchSummaries.Length); Assert.Equal(1, query.BatchSummaries.Length);
// ... The batch callback should have been called precisely 1 time
Assert.Equal(1, batchCallbacksReceived);
} }
[Fact] [Fact]
public void QueryExecuteNoOpBatch() public void QueryExecuteNoOpBatch()
{ {
// Setup:
// ... Create a callback for batch completion
Batch.BatchAsyncEventHandler batchCallback = summary =>
{
throw new Exception("Batch completion callback was called");
};
// If: // If:
// ... I create a query from a single batch that does nothing // ... I create a query from a single batch that does nothing
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Query query = new Query(Common.NoOpQuery, ci, new QueryExecutionSettings(), fileStreamFactory); Query query = new Query(Common.NoOpQuery, ci, new QueryExecutionSettings(), fileStreamFactory);
query.BatchCompleted += batchCallback;
// Then: // Then:
// ... I should get no batches back // ... I should get no batches back
@@ -117,12 +139,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
[Fact] [Fact]
public void QueryExecuteMultipleBatches() public void QueryExecuteMultipleBatches()
{ {
// Setup:
// ... Create a callback for batch completion
int batchCallbacksReceived = 0;
Batch.BatchAsyncEventHandler batchCallback = summary =>
{
batchCallbacksReceived++;
return Task.CompletedTask;
};
// If: // If:
// ... I create a query from two batches (with separator) // ... I create a query from two batches (with separator)
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false);
string queryText = string.Format("{0}\r\nGO\r\n{0}", Common.StandardQuery); string queryText = string.Format("{0}\r\nGO\r\n{0}", Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory); Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory);
query.BatchCompleted += batchCallback;
// Then: // Then:
// ... I should get back two batches to execute that haven't been executed // ... I should get back two batches to execute that haven't been executed
@@ -142,17 +174,30 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.True(query.HasExecuted); Assert.True(query.HasExecuted);
Assert.NotEmpty(query.BatchSummaries); Assert.NotEmpty(query.BatchSummaries);
Assert.Equal(2, query.BatchSummaries.Length); Assert.Equal(2, query.BatchSummaries.Length);
// ... The batch callback should have been called precisely 2 times
Assert.Equal(2, batchCallbacksReceived);
} }
[Fact] [Fact]
public void QueryExecuteMultipleBatchesWithNoOp() public void QueryExecuteMultipleBatchesWithNoOp()
{ {
// Setup:
// ... Create a callback for batch completion
int batchCallbacksReceived = 0;
Batch.BatchAsyncEventHandler batchCallback = summary =>
{
batchCallbacksReceived++;
return Task.CompletedTask;
};
// If: // If:
// ... I create a query from a two batches (with separator) // ... I create a query from a two batches (with separator)
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false); ConnectionInfo ci = Common.CreateTestConnectionInfo(null, false);
string queryText = string.Format("{0}\r\nGO\r\n{1}", Common.StandardQuery, Common.NoOpQuery); string queryText = string.Format("{0}\r\nGO\r\n{1}", Common.StandardQuery, Common.NoOpQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory); Query query = new Query(queryText, ci, new QueryExecutionSettings(), fileStreamFactory);
query.BatchCompleted += batchCallback;
// Then: // Then:
// ... I should get back one batch to execute that hasn't been executed // ... I should get back one batch to execute that hasn't been executed
@@ -171,16 +216,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.True(query.HasExecuted); Assert.True(query.HasExecuted);
Assert.NotEmpty(query.BatchSummaries); Assert.NotEmpty(query.BatchSummaries);
Assert.Equal(1, query.BatchSummaries.Length); Assert.Equal(1, query.BatchSummaries.Length);
// ... The batch callback should have been called precisely 1 time
Assert.Equal(1, batchCallbacksReceived);
} }
[Fact] [Fact]
public void QueryExecuteInvalidBatch() public void QueryExecuteInvalidBatch()
{ {
// Setup:
// ... Create a callback for batch completion
int batchCallbacksReceived = 0;
Batch.BatchAsyncEventHandler batchCallback = summary =>
{
batchCallbacksReceived++;
return Task.CompletedTask;
};
// If: // If:
// ... I create a query from an invalid batch // ... I create a query from an invalid batch
ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true); ConnectionInfo ci = Common.CreateTestConnectionInfo(null, true);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
Query query = new Query(Common.InvalidQuery, ci, new QueryExecutionSettings(), fileStreamFactory); Query query = new Query(Common.InvalidQuery, ci, new QueryExecutionSettings(), fileStreamFactory);
query.BatchCompleted += batchCallback;
// Then: // Then:
// ... I should get back a query with one batch not executed // ... I should get back a query with one batch not executed
@@ -202,6 +260,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.Equal(1, query.BatchSummaries.Length); Assert.Equal(1, query.BatchSummaries.Length);
Assert.True(query.BatchSummaries[0].HasError); Assert.True(query.BatchSummaries[0].HasError);
Assert.NotEmpty(query.BatchSummaries[0].Messages); Assert.NotEmpty(query.BatchSummaries[0].Messages);
// ... The batch callback should have been called once
Assert.Equal(1, batchCallbacksReceived);
} }
} }

View File

@@ -22,14 +22,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
{ {
// If: // If:
// ... I create a new result set with a valid db data reader // ... I create a new result set with a valid db data reader
DbDataReader mockReader = GetReader(null, false, string.Empty); DbDataReader mockReader = GetReader(null, false, string.Empty);
ResultSet resultSet = new ResultSet(mockReader, Common.GetFileStreamFactory(new Dictionary<string, byte[]>())); ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, Common.GetFileStreamFactory(new Dictionary<string, byte[]>()));
// Then: // Then:
// ... There should not be any data read yet // ... There should not be any data read yet
Assert.Null(resultSet.Columns); Assert.Null(resultSet.Columns);
Assert.Equal(0, resultSet.RowCount); Assert.Equal(0, resultSet.RowCount);
Assert.Equal(Common.Ordinal, resultSet.Id);
// ... The summary should include the same info
Assert.Null(resultSet.Summary.ColumnInfo);
Assert.Equal(0, resultSet.Summary.RowCount);
Assert.Equal(Common.Ordinal, resultSet.Summary.Id);
Assert.Equal(Common.Ordinal, resultSet.Summary.BatchId);
} }
[Fact] [Fact]
@@ -39,29 +45,45 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... I create a new result set without a reader // ... I create a new result set without a reader
// Then: // Then:
// ... It should throw an exception // ... It should throw an exception
Assert.Throws<ArgumentNullException>(() => new ResultSet(null, null)); Assert.Throws<ArgumentNullException>(() => new ResultSet(null, Common.Ordinal, Common.Ordinal, null));
} }
[Fact] [Fact]
public async Task ReadToEndSuccess() public async Task ReadToEndSuccess()
{ {
// Setup: Create a callback for resultset completion
ResultSetSummary resultSummaryFromCallback = null;
ResultSet.ResultSetAsyncEventHandler callback = r =>
{
resultSummaryFromCallback = r.Summary;
return Task.FromResult(0);
};
// If: // If:
// ... I create a new resultset with a valid db data reader that has data // ... I create a new resultset with a valid db data reader that has data
// ... and I read it to the end // ... and I read it to the end
DbDataReader mockReader = GetReader(new [] {Common.StandardTestData}, false, Common.StandardQuery); DbDataReader mockReader = GetReader(new [] {Common.StandardTestData}, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, fileStreamFactory); ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
resultSet.ResultCompletion += callback;
await resultSet.ReadResultToEnd(CancellationToken.None); await resultSet.ReadResultToEnd(CancellationToken.None);
// Then: // Then:
// ... The columns should be set // ... The columns should be set
// ... There should be rows to read back // ... There should be rows to read back
Assert.NotNull(resultSet.Columns); Assert.NotNull(resultSet.Columns);
Assert.NotEmpty(resultSet.Columns); Assert.Equal(Common.StandardColumns, resultSet.Columns.Length);
Assert.Equal(Common.StandardRows, resultSet.RowCount); Assert.Equal(Common.StandardRows, resultSet.RowCount);
}
// ... The summary should have the same info
Assert.NotNull(resultSet.Summary.ColumnInfo);
Assert.Equal(Common.StandardColumns, resultSet.Summary.ColumnInfo.Length);
Assert.Equal(Common.StandardRows, resultSet.Summary.RowCount);
// ... The callback for result set completion should have been fired
Assert.NotNull(resultSummaryFromCallback);
}
[Theory] [Theory]
[InlineData("JSON")] [InlineData("JSON")]
@@ -78,12 +100,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
} }
Dictionary<string, string>[][] dataSets = {data.ToArray()}; Dictionary<string, string>[][] dataSets = {data.ToArray()};
// ... Create a callback for resultset completion
ResultSetSummary resultSummary = null;
ResultSet.ResultSetAsyncEventHandler callback = r =>
{
resultSummary = r.Summary;
return Task.FromResult(0);
};
// If: // If:
// ... I create a new resultset with a valid db data reader that is FOR XML/JSON // ... I create a new resultset with a valid db data reader that is FOR XML/JSON
// ... and I read it to the end // ... and I read it to the end
DbDataReader mockReader = GetReader(dataSets, false, Common.StandardQuery); DbDataReader mockReader = GetReader(dataSets, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, fileStreamFactory); ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
resultSet.ResultCompletion += callback;
await resultSet.ReadResultToEnd(CancellationToken.None); await resultSet.ReadResultToEnd(CancellationToken.None);
// Then: // Then:
@@ -93,6 +124,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.Equal(1, resultSet.Columns.Length); Assert.Equal(1, resultSet.Columns.Length);
Assert.Equal(1, resultSet.RowCount); Assert.Equal(1, resultSet.RowCount);
// ... The callback should have been called
Assert.NotNull(resultSummary);
// If: // If:
// ... I attempt to read back the results // ... I attempt to read back the results
// Then: // Then:
@@ -108,7 +142,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... I create a new result set with a valid db data reader without executing it // ... I create a new result set with a valid db data reader without executing it
DbDataReader mockReader = GetReader(null, false, string.Empty); DbDataReader mockReader = GetReader(null, false, string.Empty);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, fileStreamFactory); ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
// Then: // Then:
// ... Attempting to read a subset should fail miserably // ... Attempting to read a subset should fail miserably
@@ -126,7 +160,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... And execute the result // ... And execute the result
DbDataReader mockReader = GetReader(new[] {Common.StandardTestData}, false, Common.StandardQuery); DbDataReader mockReader = GetReader(new[] {Common.StandardTestData}, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, fileStreamFactory); ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(CancellationToken.None); await resultSet.ReadResultToEnd(CancellationToken.None);
// ... And attempt to get a subset with invalid parameters // ... And attempt to get a subset with invalid parameters
@@ -147,7 +181,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... And execute the result set // ... And execute the result set
DbDataReader mockReader = GetReader(new[] { Common.StandardTestData }, false, Common.StandardQuery); DbDataReader mockReader = GetReader(new[] { Common.StandardTestData }, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>()); var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, fileStreamFactory); ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(CancellationToken.None); await resultSet.ReadResultToEnd(CancellationToken.None);
// ... And attempt to get a subset with valid number of rows // ... And attempt to get a subset with valid number of rows

View File

@@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
// //
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
@@ -20,7 +21,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
{ {
[Fact] [Fact]
public async void QueryExecuteValidNoResultsTest() public async void QueryExecuteSingleBatchNoResultsTest()
{ {
// Given: // Given:
// ... Default settings are stored in the workspace service // ... Default settings are stored in the workspace service
@@ -38,7 +39,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
QueryExecuteBatchCompleteParams batchCompleteParams = null; QueryExecuteBatchCompleteParams batchCompleteParams = null;
var requestContext = RequestContextMocks.Create<QueryExecuteResult>(qer => result = qer) var requestContext = RequestContextMocks.Create<QueryExecuteResult>(qer => result = qer)
.AddEventHandling(QueryExecuteCompleteEvent.Type, (et, p) => completeParams = p) .AddEventHandling(QueryExecuteCompleteEvent.Type, (et, p) => completeParams = p)
.AddEventHandling(QueryExecuteBatchCompleteEvent.Type, (et, p) => batchCompleteParams = p); .AddEventHandling(QueryExecuteBatchCompleteEvent.Type, (et, p) => batchCompleteParams = p)
.AddEventHandling(QueryExecuteResultSetCompleteEvent.Type, null);
await Common.AwaitExecution(queryService, queryParams, requestContext.Object); await Common.AwaitExecution(queryService, queryParams, requestContext.Object);
// Then: // Then:
@@ -46,7 +48,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... A successful result should have been sent with messages on the first batch // ... A successful result should have been sent with messages on the first batch
// ... A completion event should have been fired with empty results // ... A completion event should have been fired with empty results
// ... A batch completion event should have been fired with empty results // ... A batch completion event should have been fired with empty results
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never()); // ... A result set completion event should not have been fired
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never(), Times.Never());
Assert.Null(result.Messages); Assert.Null(result.Messages);
Assert.Equal(1, completeParams.BatchSummaries.Length); Assert.Equal(1, completeParams.BatchSummaries.Length);
@@ -56,7 +59,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.NotNull(batchCompleteParams); Assert.NotNull(batchCompleteParams);
Assert.Empty(batchCompleteParams.BatchSummary.ResultSetSummaries); Assert.Empty(batchCompleteParams.BatchSummary.ResultSetSummaries);
Assert.NotEmpty(batchCompleteParams.BatchSummary.Messages); Assert.NotEmpty(batchCompleteParams.BatchSummary.Messages);
Assert.Equal(completeParams.OwnerUri, batchCompleteParams.OwnerUri); Assert.Equal(Common.OwnerUri, batchCompleteParams.OwnerUri);
// ... There should be one active query // ... There should be one active query
Assert.Equal(1, queryService.ActiveQueries.Count); Assert.Equal(1, queryService.ActiveQueries.Count);
@@ -64,7 +67,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
[Fact] [Fact]
public async void QueryExecuteValidResultsTest() public async void QueryExecuteSingleBatchSingleResultTest()
{ {
// Given: // Given:
// ... A workspace with a standard query is configured // ... A workspace with a standard query is configured
@@ -78,16 +81,20 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
QueryExecuteResult result = null; QueryExecuteResult result = null;
QueryExecuteCompleteParams completeParams = null; QueryExecuteCompleteParams completeParams = null;
QueryExecuteBatchCompleteParams batchCompleteParams = null; QueryExecuteBatchCompleteParams batchCompleteParams = null;
QueryExecuteResultSetCompleteParams resultCompleteParams = null;
var requestContext = RequestContextMocks.Create<QueryExecuteResult>(qer => result = qer) var requestContext = RequestContextMocks.Create<QueryExecuteResult>(qer => result = qer)
.AddEventHandling(QueryExecuteCompleteEvent.Type, (et, p) => completeParams = p) .AddEventHandling(QueryExecuteCompleteEvent.Type, (et, p) => completeParams = p)
.AddEventHandling(QueryExecuteBatchCompleteEvent.Type, (et, p) => batchCompleteParams = p); .AddEventHandling(QueryExecuteBatchCompleteEvent.Type, (et, p) => batchCompleteParams = p)
.AddEventHandling(QueryExecuteResultSetCompleteEvent.Type, (et, p) => resultCompleteParams = p);
await Common.AwaitExecution(queryService, queryParams, requestContext.Object); await Common.AwaitExecution(queryService, queryParams, requestContext.Object);
// Then: // Then:
// ... No errors should have been sent // ... No errors should have been sent
// ... A successful result should have been sent with messages // ... A successful result should have been sent without messages
// ... A completion event should have been fired with one result // ... A completion event should have been fired with one result
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never()); // ... A batch completion event should have been fired
// ... A resultset completion event should have been fired
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Once(), Times.Never());
Assert.Null(result.Messages); Assert.Null(result.Messages);
Assert.Equal(1, completeParams.BatchSummaries.Length); Assert.Equal(1, completeParams.BatchSummaries.Length);
@@ -98,7 +105,124 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
Assert.NotNull(batchCompleteParams); Assert.NotNull(batchCompleteParams);
Assert.NotEmpty(batchCompleteParams.BatchSummary.ResultSetSummaries); Assert.NotEmpty(batchCompleteParams.BatchSummary.ResultSetSummaries);
Assert.NotEmpty(batchCompleteParams.BatchSummary.Messages); Assert.NotEmpty(batchCompleteParams.BatchSummary.Messages);
Assert.Equal(completeParams.OwnerUri, batchCompleteParams.OwnerUri); Assert.Equal(Common.OwnerUri, batchCompleteParams.OwnerUri);
Assert.NotNull(resultCompleteParams);
Assert.Equal(Common.StandardColumns, resultCompleteParams.ResultSetSummary.ColumnInfo.Length);
Assert.Equal(Common.StandardRows, resultCompleteParams.ResultSetSummary.RowCount);
Assert.Equal(Common.OwnerUri, resultCompleteParams.OwnerUri);
// ... There should be one active query
Assert.Equal(1, queryService.ActiveQueries.Count);
}
[Fact]
public async Task QueryExecuteSingleBatchMultipleResultTest()
{
// Given:
// ... A workspace with a standard query is configured
var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery);
// If:
// ... I request to execute a valid query with one batch and multiple result sets
var dataset = new[] { Common.StandardTestData, Common.StandardTestData };
var queryService = Common.GetPrimedExecutionService(dataset, true, false, workspaceService);
var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument };
QueryExecuteResult result = null;
QueryExecuteCompleteParams completeParams = null;
QueryExecuteBatchCompleteParams batchCompleteParams = null;
List<QueryExecuteResultSetCompleteParams> resultCompleteParams = new List<QueryExecuteResultSetCompleteParams>();
var requestContext = RequestContextMocks.Create<QueryExecuteResult>(qer => result = qer)
.AddEventHandling(QueryExecuteCompleteEvent.Type, (et, p) => completeParams = p)
.AddEventHandling(QueryExecuteBatchCompleteEvent.Type, (et, p) => batchCompleteParams = p)
.AddEventHandling(QueryExecuteResultSetCompleteEvent.Type, (et, p) => resultCompleteParams.Add(p));
await Common.AwaitExecution(queryService, queryParams, requestContext.Object);
// Then:
// ... No errors should have been sent
// ... A successful result should have been sent without messages
// ... A completion event should have been fired with one result
// ... A batch completion event should have been fired
// ... Two resultset completion events should have been fired
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Exactly(2), Times.Never());
Assert.Null(result.Messages);
Assert.Equal(1, completeParams.BatchSummaries.Length);
Assert.NotEmpty(completeParams.BatchSummaries[0].ResultSetSummaries);
Assert.NotEmpty(completeParams.BatchSummaries[0].Messages);
Assert.False(completeParams.BatchSummaries[0].HasError);
Assert.NotNull(batchCompleteParams);
Assert.NotEmpty(batchCompleteParams.BatchSummary.ResultSetSummaries);
Assert.NotEmpty(batchCompleteParams.BatchSummary.Messages);
Assert.Equal(Common.OwnerUri, batchCompleteParams.OwnerUri);
Assert.Equal(2, resultCompleteParams.Count);
foreach (var resultParam in resultCompleteParams)
{
Assert.NotNull(resultCompleteParams);
Assert.Equal(Common.StandardColumns, resultParam.ResultSetSummary.ColumnInfo.Length);
Assert.Equal(Common.StandardRows, resultParam.ResultSetSummary.RowCount);
Assert.Equal(Common.OwnerUri, resultParam.OwnerUri);
}
}
[Fact]
public async Task QueryExecuteMultipleBatchSingleResultTest()
{
// Given:
// ... A workspace with a standard query is configured
var workspaceService = Common.GetPrimedWorkspaceService(string.Format("{0}\r\nGO\r\n{0}", Common.StandardQuery));
// If:
// ... I request a to execute a valid query with multiple batches
var dataSet = new[] { Common.StandardTestData };
var queryService = Common.GetPrimedExecutionService(dataSet, true, false, workspaceService);
var queryParams = new QueryExecuteParams { OwnerUri = Common.OwnerUri, QuerySelection = Common.WholeDocument };
QueryExecuteResult result = null;
QueryExecuteCompleteParams completeParams = null;
List<QueryExecuteBatchCompleteParams> batchCompleteParams = new List<QueryExecuteBatchCompleteParams>();
List<QueryExecuteResultSetCompleteParams> resultCompleteParams = new List<QueryExecuteResultSetCompleteParams>();
var requestContext = RequestContextMocks.Create<QueryExecuteResult>(qer => result = qer)
.AddEventHandling(QueryExecuteCompleteEvent.Type, (et, p) => completeParams = p)
.AddEventHandling(QueryExecuteBatchCompleteEvent.Type, (et, p) => batchCompleteParams.Add(p))
.AddEventHandling(QueryExecuteResultSetCompleteEvent.Type, (et, p) => resultCompleteParams.Add(p));
await Common.AwaitExecution(queryService, queryParams, requestContext.Object);
// Then:
// ... No errors should have been sent
// ... A successful result should have been sent without messages
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Exactly(2), Times.Exactly(2), Times.Never());
Assert.Null(result.Messages);
// ... A completion event should have been fired with one two batch summaries, one result each
Assert.Equal(2, completeParams.BatchSummaries.Length);
Assert.Equal(1, completeParams.BatchSummaries[0].ResultSetSummaries.Length);
Assert.Equal(1, completeParams.BatchSummaries[1].ResultSetSummaries.Length);
Assert.NotEmpty(completeParams.BatchSummaries[0].Messages);
Assert.NotEmpty(completeParams.BatchSummaries[1].Messages);
// ... Two batch completion events should have been fired
Assert.Equal(2, batchCompleteParams.Count);
foreach (var batch in batchCompleteParams)
{
Assert.NotEmpty(batch.BatchSummary.ResultSetSummaries);
Assert.NotEmpty(batch.BatchSummary.Messages);
Assert.Equal(Common.OwnerUri, batch.OwnerUri);
}
// ... Two resultset completion events should have been fired
Assert.Equal(2, resultCompleteParams.Count);
foreach (var resultParam in resultCompleteParams)
{
Assert.NotNull(resultParam.ResultSetSummary);
Assert.Equal(Common.StandardColumns, resultParam.ResultSetSummary.ColumnInfo.Length);
Assert.Equal(Common.StandardRows, resultParam.ResultSetSummary.RowCount);
Assert.Equal(Common.OwnerUri, resultParam.OwnerUri);
}
// ... There should be one active query // ... There should be one active query
Assert.Equal(1, queryService.ActiveQueries.Count); Assert.Equal(1, queryService.ActiveQueries.Count);
@@ -126,7 +250,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... No result should have been returned // ... No result should have been returned
// ... No completion event should have been fired // ... No completion event should have been fired
// ... There should be no active queries // ... There should be no active queries
VerifyQueryExecuteCallCount(requestContext, Times.Never(), Times.Never(), Times.Never(), Times.Once()); VerifyQueryExecuteCallCount(requestContext, Times.Never(), Times.Never(), Times.Never(), Times.Never(), Times.Once());
Assert.IsType<string>(error); Assert.IsType<string>(error);
Assert.NotEmpty((string)error); Assert.NotEmpty((string)error);
Assert.Empty(queryService.ActiveQueries); Assert.Empty(queryService.ActiveQueries);
@@ -159,8 +283,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... An error should have been sent // ... An error should have been sent
// ... A result should have not have been sent // ... A result should have not have been sent
// ... No completion event should have been fired // ... No completion event should have been fired
// ... A batch completion event should have fired, but not a resultset event
// ... There should only be one active query // ... There should only be one active query
VerifyQueryExecuteCallCount(secondRequestContext, Times.Never(), Times.AtMostOnce(), Times.AtMostOnce(), Times.Once()); VerifyQueryExecuteCallCount(secondRequestContext, Times.Never(), Times.AtMostOnce(), Times.AtMostOnce(), Times.Never(), Times.Once());
Assert.IsType<string>(error); Assert.IsType<string>(error);
Assert.NotEmpty((string)error); Assert.NotEmpty((string)error);
Assert.Equal(1, queryService.ActiveQueries.Count); Assert.Equal(1, queryService.ActiveQueries.Count);
@@ -197,7 +322,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// ... No errors should have been sent // ... No errors should have been sent
// ... A result should have been sent with no errors // ... A result should have been sent with no errors
// ... There should only be one active query // ... There should only be one active query
VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never()); // ... A batch completion event should have fired, but not a result set completion event
VerifyQueryExecuteCallCount(secondRequestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never(), Times.Never());
Assert.Null(result.Messages); Assert.Null(result.Messages);
Assert.False(complete.BatchSummaries.Any(b => b.HasError)); Assert.False(complete.BatchSummaries.Any(b => b.HasError));
@@ -230,9 +356,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// Then: // Then:
// ... Am error should have been sent // ... Am error should have been sent
// ... No result should have been sent // ... No result should have been sent
// ... No completion event should have been fired // ... No completion events should have been fired
// ... An active query should not have been added // ... An active query should not have been added
VerifyQueryExecuteCallCount(requestContext, Times.Never(), Times.Never(), Times.Never(), Times.Once()); VerifyQueryExecuteCallCount(requestContext, Times.Never(), Times.Never(), Times.Never(), Times.Never(), Times.Once());
Assert.NotNull(errorResult); Assert.NotNull(errorResult);
Assert.IsType<string>(errorResult); Assert.IsType<string>(errorResult);
Assert.DoesNotContain(Common.OwnerUri, queryService.ActiveQueries.Keys); Assert.DoesNotContain(Common.OwnerUri, queryService.ActiveQueries.Keys);
@@ -264,8 +390,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
// Then: // Then:
// ... No errors should have been sent // ... No errors should have been sent
// ... A result should have been sent with success (we successfully started the query) // ... A result should have been sent with success (we successfully started the query)
// ... A completion event should have been sent with error // ... A completion event (query, batch, not resultset) should have been sent with error
VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never()); VerifyQueryExecuteCallCount(requestContext, Times.Once(), Times.Once(), Times.Once(), Times.Never(), Times.Never());
Assert.Null(result.Messages); Assert.Null(result.Messages);
Assert.Equal(1, complete.BatchSummaries.Length); Assert.Equal(1, complete.BatchSummaries.Length);
@@ -279,7 +405,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
} }
private static void VerifyQueryExecuteCallCount(Mock<RequestContext<QueryExecuteResult>> mock, Times sendResultCalls, private static void VerifyQueryExecuteCallCount(Mock<RequestContext<QueryExecuteResult>> mock, Times sendResultCalls,
Times sendCompletionEventCalls, Times sendBatchCompletionEvent, Times sendErrorCalls) Times sendCompletionEventCalls, Times sendBatchCompletionEvent, Times sendResultCompleteEvent, Times sendErrorCalls)
{ {
mock.Verify(rc => rc.SendResult(It.IsAny<QueryExecuteResult>()), sendResultCalls); mock.Verify(rc => rc.SendResult(It.IsAny<QueryExecuteResult>()), sendResultCalls);
mock.Verify(rc => rc.SendEvent( mock.Verify(rc => rc.SendEvent(
@@ -288,6 +414,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
mock.Verify(rc => rc.SendEvent( mock.Verify(rc => rc.SendEvent(
It.Is<EventType<QueryExecuteBatchCompleteParams>>(m => m == QueryExecuteBatchCompleteEvent.Type), It.Is<EventType<QueryExecuteBatchCompleteParams>>(m => m == QueryExecuteBatchCompleteEvent.Type),
It.IsAny<QueryExecuteBatchCompleteParams>()), sendBatchCompletionEvent); It.IsAny<QueryExecuteBatchCompleteParams>()), sendBatchCompletionEvent);
mock.Verify(rc => rc.SendEvent(
It.Is<EventType<QueryExecuteResultSetCompleteParams>>(m => m == QueryExecuteResultSetCompleteEvent.Type),
It.IsAny<QueryExecuteResultSetCompleteParams>()), sendResultCompleteEvent);
mock.Verify(rc => rc.SendError(It.IsAny<object>()), sendErrorCalls); mock.Verify(rc => rc.SendError(It.IsAny<object>()), sendErrorCalls);
} }
} }

View File

@@ -149,7 +149,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
/// Test saving results to CSV file when the requested result set is no longer active /// Test saving results to CSV file when the requested result set is no longer active
/// </summary> /// </summary>
[Fact] [Fact]
public async void SaveResultsAsCsvQueryNotFoundTest() public async Task SaveResultsAsCsvQueryNotFoundTest()
{ {
// Create a query execution service // Create a query execution service
var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery); var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery);
@@ -302,7 +302,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
/// Test saving results to JSON file when the requested result set is no longer active /// Test saving results to JSON file when the requested result set is no longer active
/// </summary> /// </summary>
[Fact] [Fact]
public async void SaveResultsAsJsonQueryNotFoundTest() public async Task SaveResultsAsJsonQueryNotFoundTest()
{ {
// Create a query service // Create a query service
var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery); var workspaceService = Common.GetPrimedWorkspaceService(Common.StandardQuery);

View File

@@ -4,6 +4,7 @@
// //
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
@@ -20,9 +21,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
#region ResultSet Class Tests #region ResultSet Class Tests
[Theory] [Theory]
[InlineData(0,2)] [InlineData(0, 2)]
[InlineData(0,20)] [InlineData(0, 20)]
[InlineData(1,2)] [InlineData(1, 2)]
public void ResultSetValidTest(int startRow, int rowCount) public void ResultSetValidTest(int startRow, int rowCount)
{ {
// Setup: // Setup:
@@ -57,6 +58,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => rs.GetSubset(rowStartIndex, rowCount)).Wait(); Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => rs.GetSubset(rowStartIndex, rowCount)).Wait();
} }
[Fact]
public async Task ResultSetNotReadTest()
{
// If:
// ... I have a resultset that hasn't been executed and I request a valid result set from it
// Then:
// ... It should throw an exception for having not been read
ResultSet rs = new ResultSet(new TestDbDataReader(null), Common.Ordinal, Common.Ordinal, Common.GetFileStreamFactory(new Dictionary<string, byte[]>()));
await Assert.ThrowsAsync<InvalidOperationException>(() => rs.GetSubset(0, 1));
}
#endregion #endregion
#region Batch Class Tests #region Batch Class Tests
@@ -92,21 +104,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => b.GetSubset(resultSetIndex, 0, 2)).Wait(); Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => b.GetSubset(resultSetIndex, 0, 2)).Wait();
} }
[Fact]
public async Task BatchSubsetIncompleteTest()
{
// If:
// ... I have a batch that hasn't completed execution
Batch b = new Batch(Common.StandardQuery, Common.WholeDocument, Common.Ordinal, Common.GetFileStreamFactory(null));
Assert.False(b.HasExecuted);
// ... And I ask for a subset
// Then:
// ... It should throw an exception
await Assert.ThrowsAsync<InvalidOperationException>(() => b.GetSubset(Common.Ordinal, 0, 2));
}
#endregion #endregion
#region Query Class Tests #region Query Class Tests
@@ -142,7 +139,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask; await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask;
// ... And I then ask for a valid set of results from it // ... 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}; var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 };
QueryExecuteSubsetResult result = null; QueryExecuteSubsetResult result = null;
var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null);
await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object); await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object);
@@ -157,7 +154,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
} }
[Fact] [Fact]
public async void SubsetServiceMissingQueryTest() public async Task SubsetServiceMissingQueryTest()
{ {
// If: // If:
// ... I ask for a set of results for a file that hasn't executed a query // ... I ask for a set of results for a file that hasn't executed a query
@@ -178,7 +175,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
} }
[Fact] [Fact]
public async void SubsetServiceUnexecutedQueryTest() public async Task SubsetServiceUnexecutedQueryTest()
{ {
// If: // If:
// ... I have a query that hasn't finished executing (doesn't matter what) // ... I have a query that hasn't finished executing (doesn't matter what)
@@ -188,13 +185,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution
var executeRequest = RequestContextMocks.Create<QueryExecuteResult>(null); var executeRequest = RequestContextMocks.Create<QueryExecuteResult>(null);
await queryService.HandleExecuteRequest(executeParams, executeRequest.Object); await queryService.HandleExecuteRequest(executeParams, executeRequest.Object);
await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask; await queryService.ActiveQueries[Common.OwnerUri].ExecutionTask;
queryService.ActiveQueries[Common.OwnerUri].HasExecuted = false; queryService.ActiveQueries[Common.OwnerUri].Batches[0].ResultSets[0].hasBeenRead = false;
// ... And I then ask for a valid set of results from it // ... 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 }; var subsetParams = new QueryExecuteSubsetParams { OwnerUri = Common.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 };
QueryExecuteSubsetResult result = null; QueryExecuteSubsetResult result = null;
var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null); var subsetRequest = GetQuerySubsetResultContextMock(qesr => result = qesr, null);
queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object).Wait(); await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object);
// Then: // Then:
// ... I should get an error result // ... I should get an error result