//
// 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.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Newtonsoft.Json;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
internal class SaveResults
{
///
/// Number of rows being read from the ResultSubset in one read
///
private const int BatchSize = 1000;
///
/// Save Task that asynchronously writes ResultSet to file
///
internal Task SaveTask { get; set; }
///
/// Event Handler for save events
///
/// Message to be returned to client
///
internal delegate Task AsyncSaveEventHandler(string message);
///
/// A successful save event
///
internal event AsyncSaveEventHandler SaveCompleted;
///
/// A failed save event
///
internal event AsyncSaveEventHandler SaveFailed;
/// Method ported from SSMS
///
/// Encodes a single field for inserting into a CSV record. The following rules are applied:
///
/// - All double quotes (") are replaced with a pair of consecutive double quotes
///
/// The entire field is also surrounded by a pair of double quotes if any of the following conditions are met:
///
/// - The field begins or ends with a space
/// - The field begins or ends with a tab
/// - The field contains the ListSeparator string
/// - The field contains the '\n' character
/// - The field contains the '\r' character
/// - The field contains the '"' character
///
///
/// The field to encode
/// The CSV encoded version of the original field
internal static string EncodeCsvField(string field)
{
StringBuilder sbField = new StringBuilder(field);
//Whether this field has special characters which require it to be embedded in quotes
bool embedInQuotes = false;
//Check for leading/trailing spaces
if (sbField.Length > 0 &&
(sbField[0] == ' ' ||
sbField[0] == '\t' ||
sbField[sbField.Length - 1] == ' ' ||
sbField[sbField.Length - 1] == '\t'))
{
embedInQuotes = true;
}
else
{ //List separator being in the field will require quotes
if (field.Contains(","))
{
embedInQuotes = true;
}
else
{
for (int i = 0; i < sbField.Length; ++i)
{
//Check whether this character is a special character
if (sbField[i] == '\r' ||
sbField[i] == '\n' ||
sbField[i] == '"')
{ //If even one character requires embedding the whole field will
//be embedded in quotes so we can just break out now
embedInQuotes = true;
break;
}
}
}
}
//Replace all quotes in the original field with double quotes
sbField.Replace("\"", "\"\"");
string ret = sbField.ToString();
if (embedInQuotes)
{
ret = "\"" + ret + "\"";
}
return ret;
}
///
/// Check if request is a subset of result set or whole result set
///
/// Parameters from the request
///
internal static bool IsSaveSelection(SaveResultsRequestParams saveParams)
{
return (saveParams.ColumnStartIndex != null && saveParams.ColumnEndIndex != null
&& saveParams.RowStartIndex != null && saveParams.RowEndIndex != null);
}
///
/// Save results as JSON format to the file specified in saveParams
///
/// Parameters from the request
/// Request context for save results
/// Result query object
///
internal void SaveResultSetAsJson(SaveResultsAsJsonRequestParams saveParams, RequestContext requestContext, Query result)
{
// Run in a separate thread
SaveTask = Task.Run(async () =>
{
try
{
using (StreamWriter jsonFile = new StreamWriter(File.Open(saveParams.FilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)))
using (JsonWriter jsonWriter = new JsonTextWriter(jsonFile))
{
int rowCount = 0;
int rowStartIndex = 0;
int columnStartIndex = 0;
int columnEndIndex = 0;
jsonWriter.Formatting = Formatting.Indented;
jsonWriter.WriteStartArray();
// Get the requested resultSet from query
Batch selectedBatch = result.Batches[saveParams.BatchIndex];
ResultSet selectedResultSet = selectedBatch.ResultSets[saveParams.ResultSetIndex];
// Set column, row counts depending on whether save request is for entire result set or a subset
if (IsSaveSelection(saveParams))
{
rowCount = saveParams.RowEndIndex.Value - saveParams.RowStartIndex.Value + 1;
rowStartIndex = saveParams.RowStartIndex.Value;
columnStartIndex = saveParams.ColumnStartIndex.Value;
columnEndIndex = saveParams.ColumnEndIndex.Value + 1; // include the last column
}
else
{
rowCount = (int)selectedResultSet.RowCount;
columnEndIndex = selectedResultSet.Columns.Length;
}
// Split rows into batches
for (int count = 0; count < (rowCount / BatchSize) + 1; count++)
{
int numberOfRows = (count < rowCount / BatchSize) ? BatchSize : (rowCount % BatchSize);
if (numberOfRows == 0)
{
break;
}
// Retrieve rows and write as json
ResultSetSubset resultSubset = await result.GetSubset(saveParams.BatchIndex, saveParams.ResultSetIndex, rowStartIndex + count * BatchSize, numberOfRows);
foreach (var row in resultSubset.Rows)
{
jsonWriter.WriteStartObject();
for (int i = columnStartIndex; i < columnEndIndex; i++)
{
// Write columnName, value pair
DbColumnWrapper col = selectedResultSet.Columns[i];
string val = row[i]?.ToString();
jsonWriter.WritePropertyName(col.ColumnName);
if (val == null)
{
jsonWriter.WriteNull();
}
else
{
jsonWriter.WriteValue(val);
}
}
jsonWriter.WriteEndObject();
}
}
jsonWriter.WriteEndArray();
}
// Successfully wrote file, send success result
if (SaveCompleted != null)
{
await SaveCompleted(null);
}
}
catch (Exception ex)
{
// Delete file when exception occurs
if (FileUtils.SafeFileExists(saveParams.FilePath))
{
FileUtils.SafeFileDelete(saveParams.FilePath);
}
if (SaveFailed != null)
{
await SaveFailed(ex.Message);
}
}
});
}
///
/// Save results as CSV format to the file specified in saveParams
///
/// Parameters from the request
/// Request context for save results
/// Result query object
///
internal void SaveResultSetAsCsv(SaveResultsAsCsvRequestParams saveParams, RequestContext requestContext, Query result)
{
// Run in a separate thread
SaveTask = Task.Run(async () =>
{
try
{
using (StreamWriter csvFile = new StreamWriter(File.Open(saveParams.FilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)))
{
ResultSetSubset resultSubset;
int columnCount = 0;
int rowCount = 0;
int columnStartIndex = 0;
int rowStartIndex = 0;
// Get the requested resultSet from query
Batch selectedBatch = result.Batches[saveParams.BatchIndex];
ResultSet selectedResultSet = (selectedBatch.ResultSets)[saveParams.ResultSetIndex];
// Set column, row counts depending on whether save request is for entire result set or a subset
if (IsSaveSelection(saveParams))
{
columnCount = saveParams.ColumnEndIndex.Value - saveParams.ColumnStartIndex.Value + 1;
rowCount = saveParams.RowEndIndex.Value - saveParams.RowStartIndex.Value + 1;
columnStartIndex = saveParams.ColumnStartIndex.Value;
rowStartIndex = saveParams.RowStartIndex.Value;
}
else
{
columnCount = selectedResultSet.Columns.Length;
rowCount = (int)selectedResultSet.RowCount;
}
// Write column names if include headers option is chosen
if (saveParams.IncludeHeaders)
{
csvFile.WriteLine(string.Join(",", selectedResultSet.Columns.Skip(columnStartIndex).Take(columnCount).Select(column =>
EncodeCsvField(column.ColumnName) ?? string.Empty)));
}
for (int i = 0; i < (rowCount / BatchSize) + 1; i++)
{
int numberOfRows = (i < rowCount / BatchSize) ? BatchSize : (rowCount % BatchSize);
if (numberOfRows == 0)
{
break;
}
// Retrieve rows and write as csv
resultSubset = await result.GetSubset(saveParams.BatchIndex, saveParams.ResultSetIndex, rowStartIndex + i * BatchSize, numberOfRows);
foreach (var row in resultSubset.Rows)
{
csvFile.WriteLine(string.Join(",", row.Skip(columnStartIndex).Take(columnCount).Select(field =>
EncodeCsvField((field != null) ? field.ToString() : "NULL"))));
}
}
}
// Successfully wrote file, send success result
if (SaveCompleted != null)
{
await SaveCompleted(null);
}
}
catch (Exception ex)
{
// Delete file when exception occurs
if (FileUtils.SafeFileExists(saveParams.FilePath))
{
FileUtils.SafeFileDelete(saveParams.FilePath);
}
if (SaveFailed != null)
{
await SaveFailed(ex.Message);
}
}
});
}
}
}