Move Save As to ResultSet (#181)

It's an overhaul of the Save As mechanism to utilize the file reader/writer classes to better align with the patterns laid out by the rest of the query execution. Why make this change? This change makes our code base more uniform and adherent to the patterns/paradigms we've set up. This change also helps with the encapsulation of the classes to "separate the concerns" of each component of the save as function. 

* Replumbing the save as execution to pass the call down the query stack as QueryExecutionService->Query->Batch->ResultSet
    * Each layer performs it's own parameter checking
        * QueryExecutionService checks if the query exists
        * Query checks if the batch exists
        * Batch checks if the result set exists
        * ResultSet checks if the row counts are valid and if the result set has been executed
    * Success/Failure delegates are passed down the chain as well
* Determination of whether a save request is a "selection" moved to the SaveResultsRequest class to eliminate duplication of code and creation of utility classes
* Making the IFileStream* classes more generic
    * Removing the requirements of max characters to store from the GetWriter method, and moving it into the constructor for the temporary buffer writer - the values have been moved to the settings and given defaults
    * Removing the individual type writers from IFileStreamWriter
    * Removing the individual type writers from IFIleStreamReader
* Adding a new overload for WriteRow to IFileStreamWriter that will write out data, given a row's worth of data and the list of columns
* Creating a new IFileStreamFactory that creates a reader/writer pair for reading from the temporary files and writing to CSV files
* Creating a new IFileStreamFactory that creates a reader/writer pair for reading from the temporary files and writing to JSON files
* Dramatically simplified the CSV encoding functionality
* Removed duplicated logic for saving in different types and condensed down to a single chain that only differs based on what type of factory is provided
* Removing the logic for managing the list of save as tasks, since the ResultSet now performs the actual saving work, there's no real need to expose the internals of the ResultSet
* Adding new strings to the sr.strings file for save as error messages
* Completely rewriting the unit tests for the save as mechanism. Very fine grained unit tests now that should cover majority of cases (aside from race conditions)


* Refactoring maxchars params into settings and out of file stream factory

* Removing write*/read* methods from file stream readers/writers

* Migrating the CSV save as to the resultset

* Tweaks to unit testing to eliminate writing files to disk

* WIP, moving to a base class for save results writers

* Everything is wired up and compiles

* Adding unit tests for CSV encoding

* Adding unit tests for CSV and Json writers

* Adding tests to the result set for saving

* Refactor to throw exceptions on errors instead of calling failure handler

* Unit tests for batch/query argument in range

* Unit tests

* Adding service integration unit tests

* Final polish, copyright notices, etc

* Adding NULL logic

* Fixing issue of unicode to utf8

* Fixing issues as per @kburtram code review comments

* Adding files that got broken?
This commit is contained in:
Benjamin Russell
2016-12-21 17:52:34 -08:00
committed by GitHub
parent adc9672fa3
commit 7ea1b1bb87
29 changed files with 1880 additions and 918 deletions

View File

@@ -24,16 +24,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
#region Constants
private const int DefaultMaxCharsToStore = 65535; // 64 KB - QE default
// xml is a special case so number of chars to store is usually greater than for other long types
private const int DefaultMaxXmlCharsToStore = 2097152; // 2 MB - QE default
// Column names of 'for xml' and 'for json' queries
private const string NameOfForXMLColumn = "XML_F52E2B61-18A1-11d1-B105-00805F49916B";
private const string NameOfForJSONColumn = "JSON_F52E2B61-18A1-11d1-B105-00805F49916B";
#endregion
#region Member Variables
@@ -74,11 +68,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
private readonly string outputFileName;
/// <summary>
/// All save tasks currently saving this ResultSet
/// </summary>
private readonly ConcurrentDictionary<string, Task> saveTasks;
#endregion
/// <summary>
@@ -104,11 +93,24 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// Store the factory
fileStreamFactory = factory;
hasBeenRead = false;
saveTasks = new ConcurrentDictionary<string, Task>();
SaveTasks = new ConcurrentDictionary<string, Task>();
}
#region Properties
/// <summary>
/// Asynchronous handler for when saving query results succeeds
/// </summary>
/// <param name="parameters">Request parameters for identifying the request</param>
public delegate Task SaveAsAsyncEventHandler(SaveResultsRequestParams parameters);
/// <summary>
/// Asynchronous handler for when saving query results fails
/// </summary>
/// <param name="parameters">Request parameters for identifying the request</param>
/// <param name="message">Message to send back describing why the request failed</param>
public delegate Task SaveAsFailureAsyncEventHandler(SaveResultsRequestParams parameters, string message);
/// <summary>
/// Asynchronous handler for when a resultset has completed
/// </summary>
@@ -141,21 +143,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
public int BatchId { get; private set; }
/// <summary>
/// Maximum number of characters to store for a field
/// </summary>
public int MaxCharsToStore { get { return DefaultMaxCharsToStore; } }
/// <summary>
/// Maximum number of characters to store for an XML field
/// </summary>
public int MaxXmlCharsToStore { get { return DefaultMaxXmlCharsToStore; } }
/// <summary>
/// The number of rows for this result set
/// </summary>
public long RowCount { get; private set; }
/// <summary>
/// All save tasks currently saving this ResultSet
/// </summary>
internal ConcurrentDictionary<string, Task> SaveTasks { get; set; }
/// <summary>
/// Generates a summary of this result set
/// </summary>
@@ -251,7 +248,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
hasBeenRead = true;
// Open a writer for the file
var fileWriter = fileStreamFactory.GetWriter(outputFileName, MaxCharsToStore, MaxCharsToStore);
var fileWriter = fileStreamFactory.GetWriter(outputFileName);
using (fileWriter)
{
// If we can initialize the columns using the column schema, use that
@@ -282,6 +279,89 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
}
public void SaveAs(SaveResultsRequestParams saveParams, IFileStreamFactory fileFactory,
SaveAsAsyncEventHandler successHandler, SaveAsFailureAsyncEventHandler failureHandler)
{
// Sanity check the save params and file factory
Validate.IsNotNull(nameof(saveParams), saveParams);
Validate.IsNotNull(nameof(fileFactory), fileFactory);
// Make sure the resultset has finished being read
if (!hasBeenRead)
{
throw new InvalidOperationException(SR.QueryServiceSaveAsResultSetNotComplete);
}
// Make sure there isn't a task for this file already
Task existingTask;
if (SaveTasks.TryGetValue(saveParams.FilePath, out existingTask))
{
if (existingTask.IsCompleted)
{
// The task has completed, so let's attempt to remove it
if (!SaveTasks.TryRemove(saveParams.FilePath, out existingTask))
{
throw new InvalidOperationException(SR.QueryServiceSaveAsMiscStartingError);
}
}
else
{
// The task hasn't completed, so we shouldn't continue
throw new InvalidOperationException(SR.QueryServiceSaveAsInProgress);
}
}
// Create the new task
Task saveAsTask = new Task(async () =>
{
try
{
// Set row counts depending on whether save request is for entire set or a subset
long rowEndIndex = RowCount;
int rowStartIndex = 0;
if (saveParams.IsSaveSelection)
{
// ReSharper disable PossibleInvalidOperationException IsSaveSelection verifies these values exist
rowEndIndex = saveParams.RowEndIndex.Value + 1;
rowStartIndex = saveParams.RowStartIndex.Value;
// ReSharper restore PossibleInvalidOperationException
}
using (var fileReader = fileFactory.GetReader(outputFileName))
using (var fileWriter = fileFactory.GetWriter(saveParams.FilePath))
{
// Iterate over the rows that are in the selected row set
for (long i = rowStartIndex; i < rowEndIndex; ++i)
{
var row = fileReader.ReadRow(fileOffsets[i], Columns);
fileWriter.WriteRow(row, Columns);
}
if (successHandler != null)
{
await successHandler(saveParams);
}
}
}
catch (Exception e)
{
fileFactory.DisposeFile(saveParams.FilePath);
if (failureHandler != null)
{
await failureHandler(saveParams, e.Message);
}
}
});
// If saving the task fails, return a failure
if (!SaveTasks.TryAdd(saveParams.FilePath, saveAsTask))
{
throw new InvalidOperationException(SR.QueryServiceSaveAsMiscStartingError);
}
// Task was saved, so start up the task
saveAsTask.Start();
}
#endregion
#region IDisposable Implementation
@@ -301,10 +381,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
IsBeingDisposed = true;
// Check if saveTasks are running for this ResultSet
if (!saveTasks.IsEmpty)
if (!SaveTasks.IsEmpty)
{
// Wait for tasks to finish before disposing ResultSet
Task.WhenAll(saveTasks.Values.ToArray()).ContinueWith((antecedent) =>
Task.WhenAll(SaveTasks.Values.ToArray()).ContinueWith((antecedent) =>
{
if (disposing)
{
@@ -357,25 +437,5 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
#endregion
#region Internal Methods to Add and Remove save tasks
internal void AddSaveTask(string key, Task saveTask)
{
saveTasks.TryAdd(key, saveTask);
}
internal void RemoveSaveTask(string key)
{
Task completedTask;
saveTasks.TryRemove(key, out completedTask);
}
internal Task GetSaveTask(string key)
{
Task completedTask;
saveTasks.TryRemove(key, out completedTask);
return completedTask;
}
#endregion
}
}