// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // #nullable disable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Common; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using NUnit.Framework; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.Execution { public class ResultSetTests { [Test] public void ResultCreation() { // If: // ... I create a new result set with a valid db data reader ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, MemoryFileSystem.GetFileStreamFactory()); // Then: // ... There should not be any data read yet Assert.Null(resultSet.Columns); Assert.AreEqual(0, resultSet.RowCount); Assert.AreEqual(Common.Ordinal, resultSet.Id); // ... The summary should include the same info Assert.Null(resultSet.Summary.ColumnInfo); Assert.AreEqual(0, resultSet.Summary.RowCount); Assert.AreEqual(Common.Ordinal, resultSet.Summary.Id); Assert.AreEqual(Common.Ordinal, resultSet.Summary.BatchId); } [Test] public async Task ReadToEndNullReader() { // If: I create a new result set with a null db data reader // Then: I should get an exception var fsf = MemoryFileSystem.GetFileStreamFactory(); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fsf); Assert.ThrowsAsync(() => resultSet.ReadResultToEnd(null, CancellationToken.None)); } /// /// Read to End test /// /// [Test] [TestCaseSource(nameof(ReadToEndSuccessData))] public async Task ReadToEndSuccess(TestResultSet[] testDataSet) { // Setup: Create a results Available callback for result set // ResultSetSummary resultSummaryFromAvailableCallback = null; Task AvailableCallback(ResultSet r) { Debug.WriteLine($"available result notification sent, result summary was: {r.Summary}"); resultSummaryFromAvailableCallback = r.Summary; return Task.CompletedTask; } // Setup: Create a results updated callback for result set // List resultSummariesFromUpdatedCallback = new List(); Task UpdatedCallback(ResultSet r) { Debug.WriteLine($"updated result notification sent, result summary was: {r.Summary}"); resultSummariesFromUpdatedCallback.Add(r.Summary); return Task.CompletedTask; } // Setup: Create a results complete callback for result set // ResultSetSummary resultSummaryFromCompleteCallback = null; Task CompleteCallback(ResultSet r) { Debug.WriteLine($"Completed result notification sent, result summary was: {r.Summary}"); resultSummaryFromCompleteCallback = r.Summary; return Task.CompletedTask; } // 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(testDataSet, false, Constants.StandardQuery); var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(testDataSet[0].Rows.Count/Common.StandardRows + 1); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory); resultSet.ResultAvailable += AvailableCallback; resultSet.ResultUpdated += UpdatedCallback; resultSet.ResultCompletion += CompleteCallback; await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); Thread.Yield(); resultSet.ResultAvailable -= AvailableCallback; resultSet.ResultUpdated -= UpdatedCallback; resultSet.ResultCompletion -= CompleteCallback; // Then: // ... The columns should be set // ... There should be rows to read back Assert.NotNull(resultSet.Columns); Assert.AreEqual(Common.StandardColumns, resultSet.Columns.Length); Assert.AreEqual(testDataSet[0].Rows.Count, resultSet.RowCount); // ... The summary should have the same info Assert.NotNull(resultSet.Summary.ColumnInfo); Assert.AreEqual(Common.StandardColumns, resultSet.Summary.ColumnInfo.Length); Assert.AreEqual(testDataSet[0].Rows.Count, resultSet.Summary.RowCount); // and: // VerifyReadResultToEnd(resultSet, resultSummaryFromAvailableCallback, resultSummaryFromCompleteCallback, resultSummariesFromUpdatedCallback); } /// /// Read to End test /// /// [Test] [TestCaseSource(nameof(ReadToEndSuccessDataParallel))] public async Task ReadToEndSuccessSeveralTimes(TestResultSet[] testDataSet) { const int NumberOfInvocations = 50; List allTasks = new List(); Parallel.ForEach(Partitioner.Create(0, NumberOfInvocations), (range) => { int start = range.Item1 == 0 ? 1 : range.Item1; Task[] tasks = new Task[range.Item2 - start]; for (int i = start; i < range.Item2; i++) { allTasks.Add(ReadToEndSuccess(testDataSet)); } }); await Task.WhenAll(allTasks); } public static readonly IEnumerable ReadToEndSuccessData = Common.TestResultSetsEnumeration.Select(r => new object[] { new TestResultSet[] { r } }).Take(6); // using all 6 sets with the parallel test can raise an OutOfMemoryException public static readonly IEnumerable ReadToEndSuccessDataParallel = Common.TestResultSetsEnumeration.Select(r => new object[] { new TestResultSet[] { r } }).Take(3); [Test] [TestCaseSource(nameof(CallMethodWithoutReadingData))] public void CallMethodWithoutReading(Action testMethod) { // Setup: Create a new result set with valid db data reader var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory); Assert.Catch(() => testMethod(resultSet), "I have a result set that has not been read. I attempt to call a method on it. It should throw an exception"); } public static IEnumerable CallMethodWithoutReadingData { get { yield return new object[] {new Action(rs => rs.GetSubset(0, 0).Wait())}; yield return new object[] {new Action(rs => rs.UpdateRow(0, null).Wait())}; yield return new object[] {new Action(rs => rs.AddRow(null).Wait())}; yield return new object[] {new Action(rs => rs.RemoveRow(0))}; yield return new object[] {new Action(rs => rs.GetRow(0))}; yield return new object[] {new Action(rs => rs.GetExecutionPlan().Wait())}; } } void VerifyReadResultToEnd(ResultSet resultSet, ResultSetSummary resultSummaryFromAvailableCallback, ResultSetSummary resultSummaryFromCompleteCallback, List resultSummariesFromUpdatedCallback) { // ... The callback for result set available, update and completion callbacks should have been fired. // Assert.True(null != resultSummaryFromCompleteCallback, "completeResultSummary is null" + $"\r\n\t\tupdateResultSets: {string.Join("\r\n\t\t\t", resultSummariesFromUpdatedCallback)}"); Assert.True(null != resultSummaryFromAvailableCallback, "availableResultSummary is null" + $"\r\n\t\tavailableResultSet: {resultSummaryFromAvailableCallback}"); // ... resultSetAvailable is not marked Complete // Assert.True(false == resultSummaryFromAvailableCallback.Complete, "availableResultSummary.Complete is true" + $"\r\n\t\tavailableResultSet: {resultSummaryFromAvailableCallback}"); // insert availableResult at the top of the resultSummariesFromUpdatedCallback list as the available result set is the first update in that series. // resultSummariesFromUpdatedCallback.Insert(0, resultSummaryFromAvailableCallback); // ... The no of rows in available result set should be non-zero // // Assert.True(0 != resultSummaryFromAvailableCallback.RowCount, "availableResultSet RowCount is 0"); // ... The final updateResultSet must have 'Complete' flag set to true // Assert.True(resultSummariesFromUpdatedCallback.Last().Complete, $"Complete Check failed.\r\n\t\t resultSummariesFromUpdatedCallback:{string.Join("\r\n\t\t\t", resultSummariesFromUpdatedCallback)}"); // ... The no of rows in the final updateResultSet/AvailableResultSet should be equal to that in the Complete Result Set. // Assert.True(resultSummaryFromCompleteCallback.RowCount == resultSummariesFromUpdatedCallback.Last().RowCount, $"The row counts of the complete Result Set and Final update result set do not match" +$"\r\n\t\tcompleteResultSet: {resultSummaryFromCompleteCallback}" +$"\r\n\t\tupdateResultSets: {string.Join("\r\n\t\t\t", resultSummariesFromUpdatedCallback)}" ); // ... RowCount should be in increasing order in updateResultSet callbacks // ..... and there should be only one resultSummary with Complete flag set to true. // int completeFlagCount = 0; Parallel.ForEach(Partitioner.Create(0, resultSummariesFromUpdatedCallback.Count), (range) => { int start = range.Item1 == 0 ? 1 : range.Item1; for (int i = start; i < range.Item2; i++) { Assert.True(resultSummariesFromUpdatedCallback[i].RowCount >= resultSummariesFromUpdatedCallback[i - 1].RowCount, $"Row Count of {i}th updateResultSet was smaller than that of the previous one" + $"\r\n\t\tupdateResultSets: {string.Join("\r\n\t\t\t", resultSummariesFromUpdatedCallback)}" ); if (resultSummariesFromUpdatedCallback[i].Complete) { Interlocked.Increment(ref completeFlagCount); } } }); Assert.True(completeFlagCount == 1, "Number of update events with complete flag event set should be 1" + $"\r\n\t\tupdateResultSets: {string.Join("\r\n\t\t\t", resultSummariesFromUpdatedCallback)}"); } /// /// Read to End Xml/JSon test /// /// /// [Test] public async Task ReadToEndForXmlJson([Values("JSON", "XML")] string forType) { // Setup: // ... Build a FOR XML or FOR JSON data set // DbColumn[] columns = {new TestDbColumn(string.Format("{0}_F52E2B61-18A1-11d1-B105-00805F49916B", forType))}; object[][] rows = Enumerable.Repeat(new object[] {"test data"}, Common.StandardRows).ToArray(); TestResultSet[] dataSets = {new TestResultSet(columns, rows) }; // Setup: Create a results Available callback for result set // ResultSetSummary resultSummaryFromAvailableCallback = null; Task AvailableCallback(ResultSet r) { Debug.WriteLine($"available result notification sent, result summary was: {r.Summary}"); resultSummaryFromAvailableCallback = r.Summary; return Task.CompletedTask; } // Setup: Create a results updated callback for result set // List resultSummariesFromUpdatedCallback = new List(); Task UpdatedCallback(ResultSet r) { Debug.WriteLine($"updated result notification sent, result summary was: {r.Summary}"); resultSummariesFromUpdatedCallback.Add(r.Summary); return Task.CompletedTask; } // Setup: Create a results complete callback for result set // ResultSetSummary resultSummaryFromCompleteCallback = null; Task CompleteCallback(ResultSet r) { Debug.WriteLine($"Completed result notification sent, result summary was: {r.Summary}"); Assert.True(r.Summary.Complete); resultSummaryFromCompleteCallback = r.Summary; return Task.CompletedTask; } // If: // ... I create a new result set with a valid db data reader that is FOR XML/JSON // ... and I read it to the end // DbDataReader mockReader = GetReader(dataSets, false, Constants.StandardQuery); var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory); resultSet.ResultAvailable += AvailableCallback; resultSet.ResultUpdated += UpdatedCallback; resultSet.ResultCompletion += CompleteCallback; var readResultTask = resultSet.ReadResultToEnd(mockReader, CancellationToken.None); await readResultTask; Debug.AutoFlush = true; Debug.Assert(readResultTask.IsCompletedSuccessfully, $"readResultTask did not Complete Successfully. Status: {readResultTask.Status}"); Thread.Yield(); resultSet.ResultAvailable -= AvailableCallback; resultSet.ResultUpdated -= UpdatedCallback; resultSet.ResultCompletion -= CompleteCallback; // Then: // ... There should only be one column // ... There should only be one row // Assert.AreEqual(1, resultSet.Columns.Length); Assert.AreEqual(1, resultSet.RowCount); // and: // VerifyReadResultToEnd(resultSet, resultSummaryFromAvailableCallback, resultSummaryFromCompleteCallback, resultSummariesFromUpdatedCallback); // If: // ... I attempt to read back the results // Then: // ... I should only get one row // var task = resultSet.GetSubset(0, 10); task.Wait(); var subset = task.Result; Assert.AreEqual(1, subset.RowCount); } [Test, Sequential] public async Task GetSubsetInvalidParameters([Values(-1,20,0)] int startRow, [Values(0,0,-1)] int rowCount) { // If: // ... I create a new result set with a valid db data reader // ... And execute the result DbDataReader mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... And attempt to get a subset with invalid parameters // Then: // ... It should throw an exception for an invalid parameter Assert.ThrowsAsync(() => resultSet.GetSubset(startRow, rowCount)); } [Test] public async Task GetSubsetSuccess([Values(0,1)]int startRow, [Values(3,20)] int rowCount) { // If: // ... I create a new result set with a valid db data reader // ... And execute the result set DbDataReader mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... And attempt to get a subset with valid number of rows ResultSetSubset subset = await resultSet.GetSubset(startRow, rowCount); // Then: // ... rows sub-array and RowCount field of the subset should match Assert.AreEqual(subset.RowCount, subset.Rows.Length); // 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.AreEqual(Math.Min(availableRowsFromStart, rowCount), subset.RowCount); // ... The rows should have the same number of columns as the resultset Assert.AreEqual(resultSet.Columns.Length, subset.Rows[0].Length); } [Test] [TestCaseSource(nameof(RowInvalidParameterData))] public async Task RowInvalidParameter(Action actionToPerform) { // If: I create a new result set and execute it var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); var fileStreamFactory = MemoryFileSystem.GetFileStreamFactory(); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileStreamFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); Assert.Catch(() => actionToPerform(resultSet), "Attempting to read an invalid row should fail"); } public static IEnumerable RowInvalidParameterData { get { foreach (var method in RowInvalidParameterMethods) { yield return new object[] {new Action(rs => method(rs, -1))}; yield return new object[] {new Action(rs => method(rs, 100))}; } } } public static IEnumerable> RowInvalidParameterMethods { get { yield return (rs, id) => rs.RemoveRow(id); yield return (rs, id) => rs.GetRow(id); yield return (rs, id) => rs.UpdateRow(id, null).Wait(); } } [Test] public async Task RemoveRowSuccess() { // Setup: Create a result set that has the standard data set on it var fileFactory = MemoryFileSystem.GetFileStreamFactory(); var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // If: I delete a row from the result set resultSet.RemoveRow(0); // Then: // ... The row count should decrease // ... The last row should have moved up by 1 Assert.AreEqual(Common.StandardRows - 1, resultSet.RowCount); Assert.Throws(() => resultSet.GetRow(Common.StandardRows - 1)); } [Test] public async Task AddRowNoRows() { // Setup: // ... Create a standard result set with standard data var fileFactory = MemoryFileSystem.GetFileStreamFactory(); var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... Create a mock reader that has no rows var emptyReader = GetReader(new[] {new TestResultSet(5, 0)}, false, Constants.StandardQuery); // If: I add a row with a reader that has no rows // Then: // ... I should get an exception Assert.ThrowsAsync(() => resultSet.AddRow(emptyReader)); // ... The row count should not have changed Assert.AreEqual(Common.StandardRows, resultSet.RowCount); } [Test] public async Task AddRowThrowsOnRead() { // Setup: // ... Create a standard result set with standard data var fileFactory = MemoryFileSystem.GetFileStreamFactory(); var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... Create a mock reader that will throw on read var throwingReader = GetReader(new[] {new TestResultSet(5, 0)}, true, Constants.StandardQuery); Assert.ThrowsAsync(() => resultSet.AddRow(throwingReader), "I add a row with a reader that throws on read. I should get an exception"); // ... The row count should not have changed Assert.AreEqual(Common.StandardRows, resultSet.RowCount); } [Test] public async Task AddRowSuccess() { // Setup: // ... Create a standard result set with standard data var fileFactory = MemoryFileSystem.GetFileStreamFactory(); var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... Create a mock reader that has one row object[] row = Enumerable.Range(0, Common.StandardColumns).Select(i => "QQQ").ToArray(); IEnumerable rows = new List{ row }; TestResultSet[] results = {new TestResultSet(TestResultSet.GetStandardColumns(Common.StandardColumns), rows)}; var newRowReader = GetReader(results, false, Constants.StandardQuery); // If: I add a new row to the result set await resultSet.AddRow(newRowReader); // Then: // ... There should be a new row in the list of rows Assert.AreEqual(Common.StandardRows + 1, resultSet.RowCount); Assert.That(resultSet.GetRow(Common.StandardRows).Select(r => r.RawObject), Has.All.EqualTo("QQQ"), "The new row should be readable and all cells contain the test value"); } [Test] public async Task UpdateRowNoRows() { // Setup: // ... Create a standard result set with standard data var fileFactory = MemoryFileSystem.GetFileStreamFactory(); var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... Create a mock reader that has no rows var emptyReader = GetReader(new[] { new TestResultSet(5, 0) }, false, Constants.StandardQuery); // If: I add a row with a reader that has no rows // Then: // ... I should get an exception Assert.ThrowsAsync(() => resultSet.UpdateRow(0, emptyReader)); // ... The row count should not have changed Assert.AreEqual(Common.StandardRows, resultSet.RowCount); } [Test] public async Task UpdateRowSuccess() { // Setup: // ... Create a standard result set with standard data var fileFactory = MemoryFileSystem.GetFileStreamFactory(); var mockReader = GetReader(Common.StandardTestDataSet, false, Constants.StandardQuery); ResultSet resultSet = new ResultSet(Common.Ordinal, Common.Ordinal, fileFactory); await resultSet.ReadResultToEnd(mockReader, CancellationToken.None); // ... Create a mock reader that has one row object[] row = Enumerable.Range(0, Common.StandardColumns).Select(i => "QQQ").ToArray(); IEnumerable rows = new List { row }; TestResultSet[] results = { new TestResultSet(TestResultSet.GetStandardColumns(Common.StandardColumns), rows) }; var newRowReader = GetReader(results, false, Constants.StandardQuery); // If: I add a new row to the result set await resultSet.UpdateRow(0, newRowReader); // Then: // ... There should be the same number of rows Assert.AreEqual(Common.StandardRows, resultSet.RowCount); Assert.That(resultSet.GetRow(0).Select(c => c.RawObject), Has.All.EqualTo("QQQ"), "The new row should be readable and all cells contain the test value"); } private static DbDataReader GetReader(TestResultSet[] dataSet, bool throwOnRead, string query) { var info = Common.CreateTestConnectionInfo(dataSet, false, throwOnRead); var connection = info.Factory.CreateSqlConnection(ConnectionService.BuildConnectionString(info.ConnectionDetails), null); var command = connection.CreateCommand(); command.CommandText = query; return command.ExecuteReader(); } } }