// // 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 } }