Files
sqltoolsservice/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs
Sharon Ravindran fea9bb43c5 Support 'for XML and for JSON' queries (#75)
* Set isXMl and isJson for 'for xml/json' resultSets

* Change string comparison

* Modify if-else
2016-10-06 11:35:29 -07:00

262 lines
9.0 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
public class ResultSet : IDisposable
{
#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
/// <summary>
/// For IDisposable pattern, whether or not object has been disposed
/// </summary>
private bool disposed;
/// <summary>
/// The factory to use to get reading/writing handlers
/// </summary>
private readonly IFileStreamFactory fileStreamFactory;
/// <summary>
/// File stream reader that will be reused to make rapid-fire retrieval of result subsets
/// quick and low perf impact.
/// </summary>
private IFileStreamReader fileStreamReader;
/// <summary>
/// Whether or not the result set has been read in from the database
/// </summary>
private bool hasBeenRead;
/// <summary>
/// The name of the temporary file we're using to output these results in
/// </summary>
private readonly string outputFileName;
#endregion
/// <summary>
/// Creates a new result set and initializes its state
/// </summary>
/// <param name="reader">The reader from executing a query</param>
/// <param name="factory">Factory for creating a reader/writer</param>
public ResultSet(DbDataReader reader, IFileStreamFactory factory)
{
// Sanity check to make sure we got a reader
Validate.IsNotNull(nameof(reader), SR.QueryServiceResultSetReaderNull);
DataReader = new StorageDataReader(reader);
// Initialize the storage
outputFileName = factory.CreateFile();
FileOffsets = new LongList<long>();
// Store the factory
fileStreamFactory = factory;
hasBeenRead = false;
}
#region Properties
/// <summary>
/// The columns for this result set
/// </summary>
public DbColumnWrapper[] Columns { get; private set; }
/// <summary>
/// The reader to use for this resultset
/// </summary>
private StorageDataReader DataReader { get; set; }
/// <summary>
/// A list of offsets into the buffer file that correspond to where rows start
/// </summary>
private LongList<long> FileOffsets { get; 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>
/// The rows of this result set
/// </summary>
public IEnumerable<string[]> Rows
{
get
{
return FileOffsets.Select(
offset => fileStreamReader.ReadRow(offset, Columns).Select(cell => cell.DisplayValue).ToArray());
}
}
#endregion
#region Public Methods
/// <summary>
/// Generates a subset of the rows from the result set
/// </summary>
/// <param name="startRow">The starting row of the results</param>
/// <param name="rowCount">How many rows to retrieve</param>
/// <returns>A subset of results</returns>
public Task<ResultSetSubset> GetSubset(int startRow, int rowCount)
{
// Sanity check to make sure that the results have been read beforehand
if (!hasBeenRead || fileStreamReader == null)
{
throw new InvalidOperationException(SR.QueryServiceResultSetNotRead);
}
// Sanity check to make sure that the row and the row count are within bounds
if (startRow < 0 || startRow >= RowCount)
{
throw new ArgumentOutOfRangeException(nameof(startRow), SR.QueryServiceResultSetStartRowOutOfRange);
}
if (rowCount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(rowCount), SR.QueryServiceResultSetRowCountOutOfRange);
}
return Task.Factory.StartNew(() =>
{
// Figure out which rows we need to read back
IEnumerable<long> rowOffsets = FileOffsets.Skip(startRow).Take(rowCount);
// Iterate over the rows we need and process them into output
string[][] rows = rowOffsets.Select(rowOffset =>
fileStreamReader.ReadRow(rowOffset, Columns).Select(cell => cell.DisplayValue).ToArray())
.ToArray();
// Retrieve the subset of the results as per the request
return new ResultSetSubset
{
Rows = rows,
RowCount = rows.Length
};
});
}
/// <summary>
/// Reads from the reader until there are no more results to read
/// </summary>
/// <param name="cancellationToken">Cancellation token for cancelling the query</param>
public async Task ReadResultToEnd(CancellationToken cancellationToken)
{
// 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
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);
}
}
// Check if resultset is 'for xml/json'. If it is, set isJson/isXml value in column metadata
SingleColumnXmlJsonResultSet();
// Mark that result has been read
hasBeenRead = true;
fileStreamReader = fileStreamFactory.GetReader(outputFileName);
}
#endregion
#region IDisposable Implementation
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
fileStreamReader?.Dispose();
fileStreamFactory.DisposeFile(outputFileName);
}
disposed = true;
}
#endregion
#region Private Helper Methods
/// <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
/// If the result set represented by this class corresponds to a single JSON
/// column that contains results of "for json" query, set isJson = true
/// </summary>
private void SingleColumnXmlJsonResultSet() {
if (Columns?.Length == 1)
{
if (Columns[0].ColumnName.Equals(NameOfForXMLColumn, StringComparison.Ordinal))
{
Columns[0].IsXml = true;
}
else if (Columns[0].ColumnName.Equals(NameOfForJSONColumn, StringComparison.Ordinal))
{
Columns[0].IsJson = true;
}
}
}
#endregion
}
}