Files
sqltoolsservice/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/Execution/ResultSetTests.cs
Benjamin Russell d9efb95386 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
2016-11-22 17:37:27 -08:00

210 lines
9.4 KiB
C#

//
// 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.Common;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution.Execution
{
public class ResultSetTests
{
[Fact]
public void ResultCreation()
{
// If:
// ... I create a new result set with a valid db data reader
DbDataReader mockReader = GetReader(null, false, string.Empty);
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, Common.GetFileStreamFactory(new Dictionary<string, byte[]>()));
// Then:
// ... There should not be any data read yet
Assert.Null(resultSet.Columns);
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]
public void ResultCreationInvalidReader()
{
// If:
// ... I create a new result set without a reader
// Then:
// ... It should throw an exception
Assert.Throws<ArgumentNullException>(() => new ResultSet(null, Common.Ordinal, Common.Ordinal, null));
}
[Fact]
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:
// ... I create a new resultset with a valid db data reader that has data
// ... and I read it to the end
DbDataReader mockReader = GetReader(new [] {Common.StandardTestData}, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
resultSet.ResultCompletion += callback;
await resultSet.ReadResultToEnd(CancellationToken.None);
// Then:
// ... The columns should be set
// ... There should be rows to read back
Assert.NotNull(resultSet.Columns);
Assert.Equal(Common.StandardColumns, resultSet.Columns.Length);
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]
[InlineData("JSON")]
[InlineData("XML")]
public async Task ReadToEndForXmlJson(string forType)
{
// Setup:
// ... Build a FOR XML or FOR JSON data set
string columnName = string.Format("{0}_F52E2B61-18A1-11d1-B105-00805F49916B", forType);
List<Dictionary<string, string>> data = new List<Dictionary<string, string>>();
for(int i = 0; i < Common.StandardRows; i++)
{
data.Add(new Dictionary<string, string> { { columnName, "test data"} });
}
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:
// ... I create a new resultset with a valid db data reader that is FOR XML/JSON
// ... and I read it to the end
DbDataReader mockReader = GetReader(dataSets, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
resultSet.ResultCompletion += callback;
await resultSet.ReadResultToEnd(CancellationToken.None);
// Then:
// ... There should only be one column
// ... There should only be one row
// ... The result should be marked as complete
Assert.Equal(1, resultSet.Columns.Length);
Assert.Equal(1, resultSet.RowCount);
// ... The callback should have been called
Assert.NotNull(resultSummary);
// If:
// ... I attempt to read back the results
// Then:
// ... I should only get one row
var subset = await resultSet.GetSubset(0, 10);
Assert.Equal(1, subset.RowCount);
}
[Fact]
public async Task GetSubsetWithoutExecution()
{
// If:
// ... I create a new result set with a valid db data reader without executing it
DbDataReader mockReader = GetReader(null, false, string.Empty);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
// Then:
// ... Attempting to read a subset should fail miserably
await Assert.ThrowsAsync<InvalidOperationException>(() => resultSet.GetSubset(0, 0));
}
[Theory]
[InlineData(-1, 0)] // Too small start row
[InlineData(20, 0)] // Too large start row
[InlineData(0, -1)] // Negative row count
public async Task GetSubsetInvalidParameters(int startRow, int rowCount)
{
// If:
// ... I create a new result set with a valid db data reader
// ... And execute the result
DbDataReader mockReader = GetReader(new[] {Common.StandardTestData}, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(CancellationToken.None);
// ... And attempt to get a subset with invalid parameters
// Then:
// ... It should throw an exception for an invalid parameter
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => resultSet.GetSubset(startRow, rowCount));
}
[Theory]
[InlineData(0, 3)] // Standard scenario, 3 rows should come back
[InlineData(0, 20)] // Asking for too many rows, 5 rows should come back
[InlineData(1, 3)] // Standard scenario from non-zero start
[InlineData(1, 20)] // Asking for too many rows at a non-zero start
public async Task GetSubsetSuccess(int startRow, int rowCount)
{
// If:
// ... I create a new result set with a valid db data reader
// ... And execute the result set
DbDataReader mockReader = GetReader(new[] { Common.StandardTestData }, false, Common.StandardQuery);
var fileStreamFactory = Common.GetFileStreamFactory(new Dictionary<string, byte[]>());
ResultSet resultSet = new ResultSet(mockReader, Common.Ordinal, Common.Ordinal, fileStreamFactory);
await resultSet.ReadResultToEnd(CancellationToken.None);
// ... And attempt to get a subset with valid number of rows
ResultSetSubset subset = await resultSet.GetSubset(startRow, rowCount);
// Then:
// ... There should be rows in the subset, either the number of rows or the number of
// rows requested or the number of rows in the result set, whichever is lower
long availableRowsFromStart = resultSet.RowCount - startRow;
Assert.Equal(Math.Min(availableRowsFromStart, rowCount), subset.RowCount);
// ... The rows should have the same number of columns as the resultset
Assert.Equal(resultSet.Columns.Length, subset.Rows[0].Length);
}
private static DbDataReader GetReader(Dictionary<string, string>[][] dataSet, bool throwOnRead, string query)
{
var info = Common.CreateTestConnectionInfo(dataSet, throwOnRead);
var connection = info.Factory.CreateSqlConnection(ConnectionService.BuildConnectionString(info.ConnectionDetails));
var command = connection.CreateCommand();
command.CommandText = query;
return command.ExecuteReader();
}
}
}