mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-22 09:35:38 -05:00
Export to Markdown Support (#1705)
* Adding file writer for Markdown tables. No testing yet. * Unit tests for the markdown writer * Wiring up the factory and and request types * Wiring up changes for Markdown serialization in serialization service * Couple last minute tweaks * Changes as per PR comments * Revert temp testing code. 🙈 * Fluent assertions in SerializationServiceTests.cs Co-authored-by: Ben Russell <russellben@microsoft.com>
This commit is contained in:
@@ -84,7 +84,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
public string Delimiter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// either CR, CRLF or LF to seperate rows in CSV
|
||||
/// either CR, CRLF or LF to separate rows in CSV
|
||||
/// </summary>
|
||||
public string LineSeperator { get; set; }
|
||||
|
||||
@@ -123,6 +123,28 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
//TODO: define config for save as JSON
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for saving results as a Markdown table
|
||||
/// </summary>
|
||||
public class SaveResultsAsMarkdownRequestParams : SaveResultsRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Encoding of the CSV file
|
||||
/// </summary>
|
||||
public string Encoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include column names as header for the table.
|
||||
/// </summary>
|
||||
public bool IncludeHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Character sequence to separate a each row in the table. Should be either CR, CRLF, or
|
||||
/// LF. If not provided, defaults to the system default line ending sequence.
|
||||
/// </summary>
|
||||
public string? LineSeparator { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to save results as XML
|
||||
/// </summary>
|
||||
@@ -179,6 +201,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
|
||||
RequestType<SaveResultsAsJsonRequestParams, SaveResultRequestResult> Type =
|
||||
RequestType<SaveResultsAsJsonRequestParams, SaveResultRequestResult>.Create("query/saveJson");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request type to save results as a Markdown table
|
||||
/// </summary>
|
||||
public class SaveResultsAsMarkdownRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<SaveResultsAsMarkdownRequestParams, SaveResultRequestResult> Type =
|
||||
RequestType<SaveResultsAsMarkdownRequestParams, SaveResultRequestResult>.Create("query/saveMarkdown");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request type to save results as XML
|
||||
|
||||
@@ -61,18 +61,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
}
|
||||
textIdentifierString = textIdentifier.ToString();
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
try
|
||||
{
|
||||
encoding = int.TryParse(requestParams.Encoding, out int codePage)
|
||||
? Encoding.GetEncoding(codePage)
|
||||
: Encoding.GetEncoding(requestParams.Encoding);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback encoding when specified codepage is invalid
|
||||
encoding = Encoding.GetEncoding("utf-8");
|
||||
}
|
||||
encoding = ParseEncoding(requestParams.Encoding, Encoding.UTF8);
|
||||
|
||||
// Output the header if the user requested it
|
||||
if (requestParams.IncludeHeaders)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
public class SaveAsMarkdownFileStreamFactory : IFileStreamFactory
|
||||
{
|
||||
private readonly SaveResultsAsMarkdownRequestParams _saveRequestParams;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs and initializes a new instance of <see cref="SaveAsMarkdownFileStreamFactory"/>.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">Parameters for the save as request</param>
|
||||
public SaveAsMarkdownFileStreamFactory(SaveResultsAsMarkdownRequestParams requestParams)
|
||||
{
|
||||
this._saveRequestParams = requestParams;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryExecutionSettings QueryExecutionSettings { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="InvalidOperationException">Throw at all times.</exception>
|
||||
[Obsolete("Not implemented for export factories.")]
|
||||
public string CreateFile()
|
||||
{
|
||||
throw new InvalidOperationException("CreateFile not implemented for export factories");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Returns an instance of the <see cref="ServiceBufferFileStreamreader"/>.
|
||||
/// </remarks>
|
||||
public IFileStreamReader GetReader(string fileName)
|
||||
{
|
||||
return new ServiceBufferFileStreamReader(
|
||||
new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite),
|
||||
this.QueryExecutionSettings);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Returns an instance of the <see cref="SaveAsMarkdownFileStreamWriter"/>.
|
||||
/// </remarks>
|
||||
public IFileStreamWriter GetWriter(string fileName, IReadOnlyList<DbColumnWrapper> columns)
|
||||
{
|
||||
return new SaveAsMarkdownFileStreamWriter(
|
||||
new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite),
|
||||
this._saveRequestParams,
|
||||
columns);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DisposeFile(string fileName)
|
||||
{
|
||||
FileUtilities.SafeFileDelete(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Writer for exporting results to a Markdown table.
|
||||
/// </summary>
|
||||
public class SaveAsMarkdownFileStreamWriter : SaveAsStreamWriter
|
||||
{
|
||||
private const string Delimiter = "|";
|
||||
private static readonly Regex NewlineRegex = new Regex(@"(\r\n|\n|\r)", RegexOptions.Compiled);
|
||||
|
||||
private readonly Encoding _encoding;
|
||||
private readonly string _lineSeparator;
|
||||
|
||||
public SaveAsMarkdownFileStreamWriter(
|
||||
Stream stream,
|
||||
SaveResultsAsMarkdownRequestParams requestParams,
|
||||
IReadOnlyList<DbColumnWrapper> columns)
|
||||
: base(stream, requestParams, columns)
|
||||
{
|
||||
// Parse the request params
|
||||
this._lineSeparator = string.IsNullOrEmpty(requestParams.LineSeparator)
|
||||
? Environment.NewLine
|
||||
: requestParams.LineSeparator;
|
||||
this._encoding = ParseEncoding(requestParams.Encoding, Encoding.UTF8);
|
||||
|
||||
// Output the header if requested
|
||||
if (requestParams.IncludeHeaders)
|
||||
{
|
||||
// Write the column header
|
||||
IEnumerable<string> selectedColumnNames = columns.Skip(this.ColumnStartIndex)
|
||||
.Take(this.ColumnCount)
|
||||
.Select(c => EncodeMarkdownField(c.ColumnName));
|
||||
string headerLine = string.Join(Delimiter, selectedColumnNames);
|
||||
|
||||
this.WriteLine($"{Delimiter}{headerLine}{Delimiter}");
|
||||
|
||||
// Write the separator row
|
||||
var separatorBuilder = new StringBuilder(Delimiter);
|
||||
for (int i = 0; i < this.ColumnCount; i++)
|
||||
{
|
||||
separatorBuilder.Append($"---{Delimiter}");
|
||||
}
|
||||
|
||||
this.WriteLine(separatorBuilder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteRow(IList<DbCellValue> row, IReadOnlyList<DbColumnWrapper> columns)
|
||||
{
|
||||
IEnumerable<string> selectedCells = row.Skip(this.ColumnStartIndex)
|
||||
.Take(this.ColumnCount)
|
||||
.Select(c => EncodeMarkdownField(c.DisplayValue));
|
||||
string rowLine = string.Join(Delimiter, selectedCells);
|
||||
|
||||
this.WriteLine($"{Delimiter}{rowLine}{Delimiter}");
|
||||
}
|
||||
|
||||
internal static string EncodeMarkdownField(string? field)
|
||||
{
|
||||
// Special case for nulls
|
||||
if (field == null)
|
||||
{
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
// Escape HTML entities, since Markdown supports inline HTML
|
||||
field = HttpUtility.HtmlEncode(field);
|
||||
|
||||
// Escape pipe delimiters
|
||||
field = field.Replace(@"|", @"\|");
|
||||
|
||||
// @TODO: Allow option to encode multiple whitespace characters as
|
||||
|
||||
// Replace newlines with br tags, since cell values must be single line
|
||||
field = NewlineRegex.Replace(field, @"<br />");
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
private void WriteLine(string line)
|
||||
{
|
||||
byte[] bytes = this._encoding.GetBytes(line + this._lineSeparator);
|
||||
this.FileStream.Write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
@@ -106,6 +107,37 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
|
||||
FileStream.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the provided <paramref name="encoding"/> and return an encoding that
|
||||
/// matches the encoding name or codepage number.
|
||||
/// </summary>
|
||||
/// <param name="encoding">Encoding name or codepage number to parse.</param>
|
||||
/// <param name="fallbackEncoding">
|
||||
/// Encoding to return if no encoding of provided name/codepage number exists.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Desired encoding object or the <paramref name="fallbackEncoding"/> if the desired
|
||||
/// encoding could not be found.
|
||||
/// </returns>
|
||||
protected static Encoding ParseEncoding(string encoding, Encoding fallbackEncoding)
|
||||
{
|
||||
// If the encoding is a number, we try to look up a codepage encoding using the
|
||||
// parsed number as a codepage. If it is not a number, attempt to look up an
|
||||
// encoding with the provided encoding name. If getting the encoding fails in
|
||||
// either case, we will return the fallback encoding.
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
try
|
||||
{
|
||||
return int.TryParse(encoding, out int codePage)
|
||||
? Encoding.GetEncoding(codePage)
|
||||
: Encoding.GetEncoding(encoding);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallbackEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool disposed;
|
||||
|
||||
@@ -98,6 +98,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
/// </summary>
|
||||
internal IFileStreamFactory JsonFileFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File factory to be used to create Markdown files from result sets.
|
||||
/// </summary>
|
||||
/// <remarks>Internal to allow overriding in unit testing.</remarks>
|
||||
internal IFileStreamFactory? MarkdownFileFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File factory to be used to create XML files from result sets. Set to internal in order
|
||||
/// to allow overriding in unit testing
|
||||
@@ -174,6 +180,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
serviceHost.SetRequestHandler(SaveResultsAsCsvRequest.Type, HandleSaveResultsAsCsvRequest);
|
||||
serviceHost.SetRequestHandler(SaveResultsAsExcelRequest.Type, HandleSaveResultsAsExcelRequest);
|
||||
serviceHost.SetRequestHandler(SaveResultsAsJsonRequest.Type, HandleSaveResultsAsJsonRequest);
|
||||
serviceHost.SetRequestHandler(SaveResultsAsMarkdownRequest.Type, this.HandleSaveResultsAsMarkdownRequest);
|
||||
serviceHost.SetRequestHandler(SaveResultsAsXmlRequest.Type, HandleSaveResultsAsXmlRequest);
|
||||
serviceHost.SetRequestHandler(QueryExecutionPlanRequest.Type, HandleExecutionPlanRequest);
|
||||
serviceHost.SetRequestHandler(SimpleExecuteRequest.Type, HandleSimpleExecuteRequest);
|
||||
@@ -518,6 +525,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
await SaveResultsHelper(saveParams, requestContext, jsonFactory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a request to save a result set to a file in Markdown format.
|
||||
/// </summary>
|
||||
/// <param name="saveParams">Parameters for the request</param>
|
||||
/// <param name="requestContext">Context of the request</param>
|
||||
internal async Task HandleSaveResultsAsMarkdownRequest(
|
||||
SaveResultsAsMarkdownRequestParams saveParams,
|
||||
RequestContext<SaveResultRequestResult> requestContext)
|
||||
{
|
||||
// Use the default markdown file factory if we haven't overridden it
|
||||
IFileStreamFactory markdownFactory = this.MarkdownFileFactory ??
|
||||
new SaveAsMarkdownFileStreamFactory(saveParams)
|
||||
{
|
||||
QueryExecutionSettings = this.Settings.QueryExecutionSettings,
|
||||
};
|
||||
|
||||
await this.SaveResultsHelper(saveParams, requestContext, markdownFactory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process request to save a resultSet to a file in XML format
|
||||
/// </summary>
|
||||
|
||||
@@ -243,6 +243,9 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
SaveRequestParams = CreateCsvRequestParams()
|
||||
};
|
||||
break;
|
||||
case "markdown":
|
||||
factory = new SaveAsMarkdownFileStreamFactory(CreateMarkdownRequestParams());
|
||||
break;
|
||||
case "xml":
|
||||
factory = new SaveAsXmlFileStreamFactory()
|
||||
{
|
||||
@@ -304,6 +307,18 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||
MaxCharsToStore = this.requestParams.MaxCharsToStore
|
||||
};
|
||||
}
|
||||
|
||||
private SaveResultsAsMarkdownRequestParams CreateMarkdownRequestParams() =>
|
||||
new SaveResultsAsMarkdownRequestParams
|
||||
{
|
||||
FilePath = this.requestParams.FilePath,
|
||||
BatchIndex = 0,
|
||||
ResultSetIndex = 0,
|
||||
IncludeHeaders = this.requestParams.IncludeHeaders,
|
||||
LineSeparator = this.requestParams.LineSeparator,
|
||||
Encoding = this.requestParams.Encoding,
|
||||
};
|
||||
|
||||
private SaveResultsAsXmlRequestParams CreateXmlRequestParams()
|
||||
{
|
||||
return new SaveResultsAsXmlRequestParams
|
||||
|
||||
Reference in New Issue
Block a user