//
// 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
///
/// For IDisposable pattern, whether or not object has been disposed
///
private bool disposed;
///
/// The factory to use to get reading/writing handlers
///
private readonly IFileStreamFactory fileStreamFactory;
///
/// File stream reader that will be reused to make rapid-fire retrieval of result subsets
/// quick and low perf impact.
///
private IFileStreamReader fileStreamReader;
///
/// Whether or not the result set has been read in from the database
///
private bool hasBeenRead;
///
/// The name of the temporary file we're using to output these results in
///
private readonly string outputFileName;
#endregion
///
/// Creates a new result set and initializes its state
///
/// The reader from executing a query
/// Factory for creating a reader/writer
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();
// Store the factory
fileStreamFactory = factory;
hasBeenRead = false;
}
#region Properties
///
/// The columns for this result set
///
public DbColumnWrapper[] Columns { get; private set; }
///
/// The reader to use for this resultset
///
private StorageDataReader DataReader { get; set; }
///
/// A list of offsets into the buffer file that correspond to where rows start
///
private LongList FileOffsets { get; set; }
///
/// Maximum number of characters to store for a field
///
public int MaxCharsToStore { get { return DefaultMaxCharsToStore; } }
///
/// Maximum number of characters to store for an XML field
///
public int MaxXmlCharsToStore { get { return DefaultMaxXmlCharsToStore; } }
///
/// The number of rows for this result set
///
public long RowCount { get; private set; }
///
/// The rows of this result set
///
public IEnumerable Rows
{
get
{
return FileOffsets.Select(
offset => fileStreamReader.ReadRow(offset, Columns).Select(cell => cell.DisplayValue).ToArray());
}
}
#endregion
#region Public Methods
///
/// Generates a subset of the rows from the result set
///
/// The starting row of the results
/// How many rows to retrieve
/// A subset of results
public Task 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 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
};
});
}
///
/// Reads from the reader until there are no more results to read
///
/// Cancellation token for cancelling the query
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
///
/// 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
///
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
}
}