mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-20 01:25:41 -05:00
Feat/result streaming (#721)
This changes adds the following two notifications from the results processing within a batch. These new notifications allows a consumer to stream results from a resultset instead of getting them all at once after the entire resultset has been fetched. ResultsAvailable This is issued after at least 1 row has been fetched for this resultset. ResultsUpdated This is issued periodically as more rows are available on this resultset. The final send of this notification when all rows have been fetched has the property 'Complete' set to true in the ResultSummary object. Detailed Change Log: * Initial completed implementation of QueryResults stream feature. 3 unittests still need fixing * Fix for the 3 failing test. I will look into making MockBehavior strict again for the three tests later * Making GetReader/GetWriter use filestream objects in FileShare.ReadWrite mode so the file can be concurrently read and written * Changing resultsAvailable also to fire off on a timer instead of after 1st row * adding a project for clr TableValuedFunction to produce result set with delays after each row. This is helpful in end to end testing. * Fixing up some tests and simplifying implementation of result update timer * Address review comments * Some test fixes * Disabled flaky test verification
This commit is contained in:
@@ -123,10 +123,21 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
/// <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
|
||||
/// called from the Batch but from the ResultSet instance.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompletion;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results. It will not be
|
||||
/// called from the Batch but from the ResultSet instance.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when additional rows in the result set are available (rowCount available has increased). It will not be
|
||||
/// called from the Batch but from the ResultSet instance.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetUpdated;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -401,6 +412,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
// This resultset has results (i.e. SELECT/etc queries)
|
||||
ResultSet resultSet = new ResultSet(resultSets.Count, Id, outputFileFactory);
|
||||
resultSet.ResultAvailable += ResultSetAvailable;
|
||||
resultSet.ResultUpdated += ResultSetUpdated;
|
||||
resultSet.ResultCompletion += ResultSetCompletion;
|
||||
|
||||
// Add the result set to the results of the query
|
||||
|
||||
@@ -49,5 +49,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// The special action of the batch
|
||||
/// </summary>
|
||||
public SpecialAction SpecialAction { get; set; }
|
||||
|
||||
public override string ToString() => $"Batch Id:'{Id}', Elapsed:'{ExecutionElapsed}', HasError:'{HasError}'";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,61 @@ using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is started or completed
|
||||
/// Base class of parameters to return when a result set is available, updated or completed
|
||||
/// </summary>
|
||||
public class ResultSetEventParams
|
||||
public abstract class ResultSetEventParams
|
||||
{
|
||||
public ResultSetSummary ResultSetSummary { get; set; }
|
||||
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
|
||||
public class ResultSetCompleteEvent
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is completed.
|
||||
/// </summary>
|
||||
public class ResultSetCompleteEventParams : ResultSetEventParams
|
||||
{
|
||||
public static readonly
|
||||
EventType<ResultSetEventParams> Type =
|
||||
EventType<ResultSetEventParams>.Create("query/resultSetComplete");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is available.
|
||||
/// </summary>
|
||||
public class ResultSetAvailableEventParams : ResultSetEventParams
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to return when a result set is updated
|
||||
/// </summary>
|
||||
public class ResultSetUpdatedEventParams : ResultSetEventParams
|
||||
{
|
||||
}
|
||||
|
||||
public class ResultSetCompleteEvent
|
||||
{
|
||||
public static string MethodName { get; } = "query/resultSetComplete";
|
||||
|
||||
public static readonly
|
||||
EventType<ResultSetCompleteEventParams> Type =
|
||||
EventType<ResultSetCompleteEventParams>.Create(MethodName);
|
||||
}
|
||||
|
||||
public class ResultSetAvailableEvent
|
||||
{
|
||||
public static string MethodName { get; } = "query/resultSetAvailable";
|
||||
|
||||
public static readonly
|
||||
EventType<ResultSetAvailableEventParams> Type =
|
||||
EventType<ResultSetAvailableEventParams>.Create(MethodName);
|
||||
}
|
||||
|
||||
public class ResultSetUpdatedEvent
|
||||
{
|
||||
public static string MethodName { get; } = "query/resultSetUpdated";
|
||||
|
||||
public static readonly
|
||||
EventType<ResultSetUpdatedEventParams> Type =
|
||||
EventType<ResultSetUpdatedEventParams>.Create(MethodName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,5 +51,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
public ResultMessage()
|
||||
{
|
||||
}
|
||||
public override string ToString() => $"Message on Batch Id:'{BatchId}', IsError:'{IsError}', Message:'{Message}'";
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
public int BatchId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows that was returned with the resultset
|
||||
/// The number of rows that are available for the resultset thus far
|
||||
/// </summary>
|
||||
public long RowCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true it indicates that all rows have been fetched and the RowCount being sent across is final for this ResultSet
|
||||
/// </summary>
|
||||
public bool Complete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Details about the columns that are provided as solutions
|
||||
/// </summary>
|
||||
@@ -35,5 +40,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
/// </summary>
|
||||
public SpecialAction SpecialAction { get; set; }
|
||||
|
||||
public override string ToString() => $"Result Summary Id:{Id}, Batch Id:'{BatchId}', RowCount:'{RowCount}', Complete:'{Complete}', SpecialAction:'{SpecialAction}'";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,23 +42,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read), QueryExecutionSettings);
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new CSV writer for writing results to a CSV file
|
||||
/// Returns a new CSV writer for writing results to a CSV file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the CSV output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsCsvFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite), SaveRequestParams);
|
||||
return new SaveAsCsvFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -42,23 +42,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read), QueryExecutionSettings);
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new Excel writer for writing results to a Excel file
|
||||
/// Returns a new Excel writer for writing results to a Excel file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the Excel output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsExcelFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite), SaveRequestParams);
|
||||
return new SaveAsExcelFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,23 +39,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read), QueryExecutionSettings);
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new JSON writer for writing results to a JSON file
|
||||
/// Returns a new JSON writer for writing results to a JSON file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the JSON output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsJsonFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite), SaveRequestParams);
|
||||
return new SaveAsJsonFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,23 +39,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files
|
||||
/// Returns a new service buffer reader for reading results back in from the temporary buffer files, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the temp buffer file</param>
|
||||
/// <returns>Stream reader</returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read), QueryExecutionSettings);
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new XML writer for writing results to a XML file
|
||||
/// Returns a new XML writer for writing results to a XML file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Path to the XML output file</param>
|
||||
/// <returns>Stream writer</returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new SaveAsXmlFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite), SaveRequestParams);
|
||||
return new SaveAsXmlFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), SaveRequestParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -34,24 +34,24 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ServiceBufferFileStreamReader"/> for reading values back from
|
||||
/// an SSMS formatted buffer file
|
||||
/// an SSMS formatted buffer file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file to read values from</param>
|
||||
/// <returns>A <see cref="ServiceBufferFileStreamReader"/></returns>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read), ExecutionSettings);
|
||||
return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), ExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ServiceBufferFileStreamWriter"/> for writing values out to an
|
||||
/// SSMS formatted buffer file
|
||||
/// SSMS formatted buffer file, file share is ReadWrite to allow concurrent reads/writes to the file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file to write values to</param>
|
||||
/// <returns>A <see cref="ServiceBufferFileStreamWriter"/></returns>
|
||||
public IFileStreamWriter GetWriter(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite), ExecutionSettings);
|
||||
return new ServiceBufferFileStreamWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ExecutionSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
@@ -190,6 +191,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results.
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when additional rows in the result set are available (rowCount available has increased)
|
||||
/// </summary>
|
||||
public event ResultSet.ResultSetAsyncEventHandler ResultSetUpdated;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -298,6 +308,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <returns>A subset of results</returns>
|
||||
public Task<ResultSetSubset> GetSubset(int batchIndex, int resultSetIndex, long startRow, int rowCount)
|
||||
{
|
||||
Logger.Write(TraceEventType.Start, $"Starting GetSubset execution for batchIndex:'{batchIndex}', resultSetIndex:'{resultSetIndex}', startRow:'{startRow}', rowCount:'{rowCount}'");
|
||||
// Sanity check to make sure that the batch is within bounds
|
||||
if (batchIndex < 0 || batchIndex >= Batches.Length)
|
||||
{
|
||||
@@ -399,6 +410,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
b.BatchCompletion += BatchCompleted;
|
||||
b.BatchMessageSent += BatchMessageSent;
|
||||
b.ResultSetCompletion += ResultSetCompleted;
|
||||
b.ResultSetAvailable += ResultSetAvailable;
|
||||
b.ResultSetUpdated += ResultSetUpdated;
|
||||
await b.Execute(queryConnection, cancellationSource.Token);
|
||||
}
|
||||
|
||||
|
||||
@@ -183,9 +183,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
// Setup actions to perform upon successful start and on failure to start
|
||||
Func<Query, Task<bool>> queryCreateSuccessAction = async q => {
|
||||
await requestContext.SendResult(new ExecuteRequestResult());
|
||||
Logger.Write(TraceEventType.Stop, $"Response for Query: '{executeParams.OwnerUri} sent. Query Complete!");
|
||||
return true;
|
||||
};
|
||||
Func<string, Task> queryCreateFailureAction = message => requestContext.SendError(message);
|
||||
Func<string, Task> queryCreateFailureAction = message =>
|
||||
{
|
||||
Logger.Write(TraceEventType.Warning, $"Failed to create Query: '{executeParams.OwnerUri}. Message: '{message}' Complete!");
|
||||
return requestContext.SendError(message);
|
||||
};
|
||||
|
||||
// Use the internal handler to launch the query
|
||||
return InterServiceExecuteQuery(executeParams, null, requestContext, queryCreateSuccessAction, queryCreateFailureAction, null, null);
|
||||
@@ -321,6 +326,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
ResultSubset = subset
|
||||
};
|
||||
await requestContext.SendResult(result);
|
||||
Logger.Write(TraceEventType.Stop, $"Done Handler for Subset request with for Query:'{subsetParams.OwnerUri}', Batch:'{subsetParams.BatchIndex}', ResultSetIndex:'{subsetParams.ResultSetIndex}', RowsStartIndex'{subsetParams.RowsStartIndex}', Requested RowsCount:'{subsetParams.RowsCount}'\r\n\t\t with subset response of:[ RowCount:'{subset.RowCount}', Rows array of length:'{subset.Rows.Length}']");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -612,6 +618,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
// Attempt to clean out any old query on the owner URI
|
||||
Query oldQuery;
|
||||
// DevNote:
|
||||
// if any oldQuery exists on the executeParams.OwnerUri but it has not yet executed,
|
||||
// then shouldn't we cancel and clean out that query since we are about to create a new query object on the current OwnerUri.
|
||||
//
|
||||
if (ActiveQueries.TryGetValue(executeParams.OwnerUri, out oldQuery) && oldQuery.HasExecuted)
|
||||
{
|
||||
oldQuery.Dispose();
|
||||
@@ -632,6 +642,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
throw new InvalidOperationException(SR.QueryServiceQueryInProgress);
|
||||
}
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Query object for URI:'{executeParams.OwnerUri}' created");
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
@@ -650,10 +661,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
BatchSummaries = q.BatchSummaries
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Query:'{ownerUri}' completed");
|
||||
await eventSender.SendEvent(QueryCompleteEvent.Type, eventParams);
|
||||
};
|
||||
|
||||
// Setup the callback to send the complete event
|
||||
// Setup the callback to send the failure event
|
||||
Query.QueryAsyncErrorEventHandler failureCallback = async (q, e) =>
|
||||
{
|
||||
// Send back the results
|
||||
@@ -663,6 +675,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
BatchSummaries = q.BatchSummaries
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Error, $"Query:'{ownerUri}' failed");
|
||||
await eventSender.SendEvent(QueryCompleteEvent.Type, eventParams);
|
||||
};
|
||||
query.QueryCompleted += completeCallback;
|
||||
@@ -682,6 +695,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
OwnerUri = ownerUri
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Batch:'{b.Summary}' on Query:'{ownerUri}' started");
|
||||
await eventSender.SendEvent(BatchStartEvent.Type, eventParams);
|
||||
};
|
||||
query.BatchStarted += batchStartCallback;
|
||||
@@ -694,6 +708,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
OwnerUri = ownerUri
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Batch:'{b.Summary}' on Query:'{ownerUri}' completed");
|
||||
await eventSender.SendEvent(BatchCompleteEvent.Type, eventParams);
|
||||
};
|
||||
query.BatchCompleted += batchCompleteCallback;
|
||||
@@ -705,21 +720,53 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
Message = m,
|
||||
OwnerUri = ownerUri
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Message generated on Query:'{ownerUri}' :'{m}'");
|
||||
await eventSender.SendEvent(MessageEvent.Type, eventParams);
|
||||
};
|
||||
query.BatchMessageSent += batchMessageCallback;
|
||||
|
||||
// Setup the ResultSet completion callback
|
||||
ResultSet.ResultSetAsyncEventHandler resultCallback = async r =>
|
||||
// Setup the ResultSet available callback
|
||||
ResultSet.ResultSetAsyncEventHandler resultAvailableCallback = async r =>
|
||||
{
|
||||
ResultSetEventParams eventParams = new ResultSetEventParams
|
||||
ResultSetAvailableEventParams eventParams = new ResultSetAvailableEventParams
|
||||
{
|
||||
ResultSetSummary = r.Summary,
|
||||
OwnerUri = ownerUri
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Result:'{r.Summary} on Query:'{ownerUri}' is available");
|
||||
await eventSender.SendEvent(ResultSetAvailableEvent.Type, eventParams);
|
||||
};
|
||||
query.ResultSetAvailable += resultAvailableCallback;
|
||||
|
||||
// Setup the ResultSet updated callback
|
||||
ResultSet.ResultSetAsyncEventHandler resultUpdatedCallback = async r =>
|
||||
{
|
||||
ResultSetUpdatedEventParams eventParams = new ResultSetUpdatedEventParams
|
||||
{
|
||||
ResultSetSummary = r.Summary,
|
||||
OwnerUri = ownerUri
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Result:'{r.Summary} on Query:'{ownerUri}' is updated with additional rows");
|
||||
await eventSender.SendEvent(ResultSetUpdatedEvent.Type, eventParams);
|
||||
};
|
||||
query.ResultSetUpdated += resultUpdatedCallback;
|
||||
|
||||
// Setup the ResultSet completion callback
|
||||
ResultSet.ResultSetAsyncEventHandler resultCompleteCallback = async r =>
|
||||
{
|
||||
ResultSetCompleteEventParams eventParams = new ResultSetCompleteEventParams
|
||||
{
|
||||
ResultSetSummary = r.Summary,
|
||||
OwnerUri = ownerUri
|
||||
};
|
||||
|
||||
Logger.Write(TraceEventType.Information, $"Result:'{r.Summary} on Query:'{ownerUri}' is complete");
|
||||
await eventSender.SendEvent(ResultSetCompleteEvent.Type, eventParams);
|
||||
};
|
||||
query.ResultSetCompleted += resultCallback;
|
||||
query.ResultSetCompleted += resultCompleteCallback;
|
||||
|
||||
// Launch this as an asynchronous task
|
||||
query.Execute();
|
||||
|
||||
@@ -1,20 +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 System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
@@ -30,7 +32,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
private const string NameOfForXmlColumn = "XML_F52E2B61-18A1-11d1-B105-00805F49916B";
|
||||
private const string NameOfForJsonColumn = "JSON_F52E2B61-18A1-11d1-B105-00805F49916B";
|
||||
private const string YukonXmlShowPlanColumn = "Microsoft SQL Server 2005 XML Showplan";
|
||||
|
||||
private const uint MaxResultsTimerPulseMilliseconds = 1000;
|
||||
private const uint MinResultTimerPulseMilliseconds = 10;
|
||||
#endregion
|
||||
|
||||
#region Member Variables
|
||||
@@ -52,9 +55,15 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the result set has been read in from the database,
|
||||
/// set as internal in order to fake value in unit tests
|
||||
/// set as internal in order to fake value in unit tests.
|
||||
/// This gets set as soon as we start reading.
|
||||
/// </summary>
|
||||
internal bool hasBeenRead;
|
||||
internal bool hasStartedRead = false;
|
||||
|
||||
/// <summary>
|
||||
/// Set when all results have been read for this resultSet from the server
|
||||
/// </summary>
|
||||
private bool hasCompletedRead = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether resultSet is a 'for xml' or 'for json' result
|
||||
@@ -69,7 +78,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <summary>
|
||||
/// Row count to use in special scenarios where we want to override the number of rows.
|
||||
/// </summary>
|
||||
private long? rowCountOverride;
|
||||
private long? rowCountOverride=null;
|
||||
|
||||
/// <summary>
|
||||
/// The special action which applied to this result set
|
||||
@@ -82,6 +91,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
internal long totalBytesWritten;
|
||||
|
||||
internal readonly Timer resultsTimer;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -103,8 +114,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
// Store the factory
|
||||
fileStreamFactory = factory;
|
||||
hasBeenRead = false;
|
||||
hasStartedRead = false;
|
||||
hasCompletedRead = false;
|
||||
SaveTasks = new ConcurrentDictionary<string, Task>();
|
||||
resultsTimer = new Timer(SendResultAvailableOrUpdated);
|
||||
}
|
||||
|
||||
#region Eventing
|
||||
@@ -123,7 +136,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
public delegate Task SaveAsFailureAsyncEventHandler(SaveResultsRequestParams parameters, string message);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous handler for when a resultset has completed
|
||||
/// Asynchronous handler for when a resultset is available/updated/completed
|
||||
/// </summary>
|
||||
/// <param name="resultSet">The result set that completed</param>
|
||||
public delegate Task ResultSetAsyncEventHandler(ResultSet resultSet);
|
||||
@@ -133,6 +146,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
public event ResultSetAsyncEventHandler ResultCompletion;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when the resultSet first becomes available. This is as soon as we start reading the results.
|
||||
/// </summary>
|
||||
public event ResultSetAsyncEventHandler ResultAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called when additional rows in the result set are available (rowCount available has increased)
|
||||
/// </summary>
|
||||
public event ResultSetAsyncEventHandler ResultUpdated;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -155,7 +179,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <summary>
|
||||
/// The number of rows for this result set
|
||||
/// </summary>
|
||||
public long RowCount => rowCountOverride ?? fileOffsets.Count;
|
||||
public long RowCount => rowCountOverride != null ? Math.Min(rowCountOverride.Value, fileOffsets.Count) : fileOffsets.Count;
|
||||
|
||||
/// <summary>
|
||||
/// All save tasks currently saving this ResultSet
|
||||
@@ -175,11 +199,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
Id = Id,
|
||||
BatchId = BatchId,
|
||||
RowCount = RowCount,
|
||||
SpecialAction = hasBeenRead ? ProcessSpecialAction() : null
|
||||
Complete = hasCompletedRead,
|
||||
SpecialAction = hasCompletedRead ? ProcessSpecialAction() : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
@@ -195,8 +219,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <returns>The requested row</returns>
|
||||
public IList<DbCellValue> GetRow(long rowId)
|
||||
{
|
||||
// Sanity check to make sure that results have been read beforehand
|
||||
if (!hasBeenRead)
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
@@ -221,8 +245,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <returns>A subset of results</returns>
|
||||
public Task<ResultSetSubset> GetSubset(long startRow, int rowCount)
|
||||
{
|
||||
// Sanity check to make sure that the results have been read beforehand
|
||||
if (!hasBeenRead)
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
@@ -286,11 +310,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <returns>An execution plan object</returns>
|
||||
public Task<ExecutionPlan> GetExecutionPlan()
|
||||
{
|
||||
// Process the action just incase is hasn't been yet
|
||||
// Process the action just in case it hasn't been yet
|
||||
ProcessSpecialAction();
|
||||
|
||||
// Sanity check to make sure that the results have been read beforehand
|
||||
if (!hasBeenRead)
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
@@ -333,6 +357,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
public async Task ReadResultToEnd(DbDataReader dbDataReader, CancellationToken cancellationToken)
|
||||
{
|
||||
// Sanity check to make sure we got a reader
|
||||
//
|
||||
Validate.IsNotNull(nameof(dbDataReader), dbDataReader);
|
||||
|
||||
try
|
||||
@@ -340,38 +365,57 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
// Verify the request hasn't been cancelled
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Mark that result has been read
|
||||
hasBeenRead = true;
|
||||
|
||||
StorageDataReader dataReader = new StorageDataReader(dbDataReader);
|
||||
|
||||
// Open a writer for the file
|
||||
//
|
||||
var fileWriter = fileStreamFactory.GetWriter(outputFileName);
|
||||
using (fileWriter)
|
||||
{
|
||||
// If we can initialize the columns using the column schema, use that
|
||||
//
|
||||
if (!dataReader.DbDataReader.CanGetColumnSchema())
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNoColumnSchema);
|
||||
}
|
||||
Columns = dataReader.Columns;
|
||||
// Check if result set is 'for xml/json'. If it is, set isJson/isXml value in column metadata
|
||||
//
|
||||
SingleColumnXmlJsonResultSet();
|
||||
|
||||
// Mark that read of result has started
|
||||
//
|
||||
hasStartedRead = true;
|
||||
while (await dataReader.ReadAsync(cancellationToken))
|
||||
{
|
||||
fileOffsets.Add(totalBytesWritten);
|
||||
totalBytesWritten += fileWriter.WriteRow(dataReader);
|
||||
|
||||
// If we have never triggered the timer to start sending the results available/updated notification
|
||||
// then: Trigger the timer to start sending results update notification
|
||||
//
|
||||
if (LastUpdatedSummary == null)
|
||||
{
|
||||
// Invoke the timer to send available/update result set notification immediately
|
||||
//
|
||||
resultsTimer.Change(0, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
CheckForIsJson();
|
||||
}
|
||||
// Check if resultset is 'for xml/json'. If it is, set isJson/isXml value in column metadata
|
||||
SingleColumnXmlJsonResultSet();
|
||||
CheckForIsJson();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Fire off a result set completion event if we have one
|
||||
if (ResultCompletion != null)
|
||||
{
|
||||
await ResultCompletion(this);
|
||||
}
|
||||
hasCompletedRead = true; // set the flag to indicate that we are done reading
|
||||
|
||||
// Make a final call to ResultUpdated by invoking the timer to send update result set notification immediately
|
||||
//
|
||||
resultsTimer.Change(0, Timeout.Infinite);
|
||||
|
||||
// and finally:
|
||||
// Make a call to send ResultCompletion and await for it to Complete
|
||||
//
|
||||
await (ResultCompletion?.Invoke(this) ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,8 +425,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// <param name="internalId">Internal ID of the row</param>
|
||||
public void RemoveRow(long internalId)
|
||||
{
|
||||
// Make sure that the results have been read
|
||||
if (!hasBeenRead)
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
@@ -436,7 +480,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
Validate.IsNotNull(nameof(fileFactory), fileFactory);
|
||||
|
||||
// Make sure the resultset has finished being read
|
||||
if (!hasBeenRead)
|
||||
if (!hasCompletedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceSaveAsResultSetNotComplete);
|
||||
}
|
||||
@@ -526,6 +570,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
resultsTimer.Dispose();
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
@@ -564,7 +609,59 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
#endregion
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Sends the ResultsUpdated message if the number of rows has changed since last send.
|
||||
/// </summary>
|
||||
/// <param name="stateInfo"></param>
|
||||
private async void SendResultAvailableOrUpdated (object stateInfo = null)
|
||||
{
|
||||
ResultSet currentResultSetSnapshot = (ResultSet) MemberwiseClone();
|
||||
if (LastUpdatedSummary == null) // We need to send results available message.
|
||||
{
|
||||
// Fire off results Available task and await for it complete
|
||||
//
|
||||
await (ResultAvailable?.Invoke(currentResultSetSnapshot) ?? Task.CompletedTask);
|
||||
ResultAvailable = null; // set this to null as we need to call ResultAvailable only once
|
||||
}
|
||||
else // We need to send results updated message.
|
||||
{
|
||||
// If there has been no change in rowCount since last update and we are not done yet then log and increase the timer duration
|
||||
//
|
||||
if (!currentResultSetSnapshot.hasCompletedRead && LastUpdatedSummary.RowCount == currentResultSetSnapshot.RowCount)
|
||||
{
|
||||
Logger.Write(TraceEventType.Warning, $"The result set:{Summary} has not made any progress in last {ResultTimerInterval} milliseconds and the read of resultset is not completed yet!");
|
||||
ResultsIntervalMultiplier++;
|
||||
}
|
||||
|
||||
// Fire off results updated task and await for it complete
|
||||
//
|
||||
await (ResultUpdated?.Invoke(currentResultSetSnapshot) ?? Task.CompletedTask);
|
||||
|
||||
}
|
||||
|
||||
// Update the LastUpdatedSummary to be the value captured in current snapshot
|
||||
//
|
||||
LastUpdatedSummary = currentResultSetSnapshot.Summary;
|
||||
|
||||
// Setup timer for the next callback
|
||||
if (currentResultSetSnapshot.hasCompletedRead)
|
||||
{
|
||||
//If we have already completed reading then we are done and we do not need to send any more updates. Switch off timer.
|
||||
//
|
||||
resultsTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultsTimer.Change(ResultTimerInterval, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
|
||||
private uint ResultsIntervalMultiplier { get; set; } = 1;
|
||||
|
||||
internal uint ResultTimerInterval => Math.Max(Math.Min(MaxResultsTimerPulseMilliseconds, (uint)RowCount / 500 /* 1 millisec per 500 rows*/), MinResultTimerPulseMilliseconds * ResultsIntervalMultiplier);
|
||||
|
||||
internal ResultSetSummary LastUpdatedSummary { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// If the result set represented by this class corresponds to a single XML
|
||||
/// column that contains results of "for xml" query, set isXml = true
|
||||
@@ -574,7 +671,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
private void SingleColumnXmlJsonResultSet()
|
||||
{
|
||||
|
||||
if (Columns?.Length == 1 && RowCount != 0)
|
||||
if (Columns?.Length == 1)
|
||||
{
|
||||
if (Columns[0].ColumnName.Equals(NameOfForXmlColumn, StringComparison.Ordinal))
|
||||
{
|
||||
@@ -636,7 +733,8 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
private async Task<long> AppendRowToBuffer(DbDataReader dbDataReader)
|
||||
{
|
||||
Validate.IsNotNull(nameof(dbDataReader), dbDataReader);
|
||||
if (!hasBeenRead)
|
||||
// Sanity check to make sure that results read has started
|
||||
if (!hasStartedRead)
|
||||
{
|
||||
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
{
|
||||
flags |= action.flags;
|
||||
}
|
||||
|
||||
public override string ToString() => $"ActionFlag:'{flags}', ExpectYukonXMLShowPlan:'{ExpectYukonXMLShowPlan}'";
|
||||
#endregion
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user