mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 09:59:48 -05:00
* Make nullable warnings a per file opt-in * Remove unneeded compiler directives * Remove compiler directive for User Data
336 lines
13 KiB
C#
336 lines
13 KiB
C#
//
|
|
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
//
|
|
|
|
#nullable disable
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Composition;
|
|
using System.Diagnostics;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.SqlTools.Extensibility;
|
|
using Microsoft.SqlTools.Hosting;
|
|
using Microsoft.SqlTools.Hosting.Protocol;
|
|
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
|
|
using Microsoft.SqlTools.ServiceLayer.Utility;
|
|
using Microsoft.SqlTools.Utility;
|
|
|
|
|
|
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
|
{
|
|
|
|
[Export(typeof(IHostedService))]
|
|
public class SerializationService : HostedService<SerializationService>, IComposableService
|
|
{
|
|
private ConcurrentDictionary<string, DataSerializer> inProgressSerializations;
|
|
|
|
public SerializationService()
|
|
{
|
|
inProgressSerializations = new ConcurrentDictionary<string, DataSerializer>();
|
|
}
|
|
|
|
public override void InitializeService(IProtocolEndpoint serviceHost)
|
|
{
|
|
Logger.Write(TraceEventType.Verbose, "SerializationService initialized");
|
|
serviceHost.SetRequestHandler(SerializeStartRequest.Type, HandleSerializeStartRequest, true);
|
|
serviceHost.SetRequestHandler(SerializeContinueRequest.Type, HandleSerializeContinueRequest, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begin to process request to save a resultSet to a file in CSV format
|
|
/// </summary>
|
|
internal Task HandleSerializeStartRequest(SerializeDataStartRequestParams serializeParams,
|
|
RequestContext<SerializeDataResult> requestContext)
|
|
{
|
|
// Run in separate thread so that message thread isn't held up by a potentially time consuming file write
|
|
Task.Run(async () => {
|
|
await RunSerializeStartRequest(serializeParams, requestContext);
|
|
}).ContinueWithOnFaulted(async t => await SendErrorAndCleanup(serializeParams?.FilePath, requestContext, t.Exception));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
internal async Task RunSerializeStartRequest(SerializeDataStartRequestParams serializeParams, RequestContext<SerializeDataResult> requestContext)
|
|
{
|
|
try
|
|
{
|
|
// Verify we have sensible inputs and there isn't a task running for this file already
|
|
Validate.IsNotNull(nameof(serializeParams), serializeParams);
|
|
Validate.IsNotNullOrWhitespaceString("FilePath", serializeParams.FilePath);
|
|
|
|
DataSerializer serializer = null;
|
|
if (inProgressSerializations.TryGetValue(serializeParams.FilePath, out serializer))
|
|
{
|
|
// Cannot proceed as there is an in progress serialization happening
|
|
throw new Exception(SR.SerializationServiceRequestInProgress(serializeParams.FilePath));
|
|
}
|
|
|
|
// Create a new serializer, save for future calls if needed, and write the request out
|
|
serializer = new DataSerializer(serializeParams);
|
|
if (!serializeParams.IsLastBatch)
|
|
{
|
|
inProgressSerializations.AddOrUpdate(serializer.FilePath, serializer, (key, old) => serializer);
|
|
}
|
|
|
|
Logger.Write(TraceEventType.Verbose, "HandleSerializeStartRequest");
|
|
SerializeDataResult result = serializer.ProcessRequest(serializeParams);
|
|
await requestContext.SendResult(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await SendErrorAndCleanup(serializeParams.FilePath, requestContext, ex);
|
|
}
|
|
}
|
|
|
|
private async Task SendErrorAndCleanup(string filePath, RequestContext<SerializeDataResult> requestContext, Exception ex)
|
|
{
|
|
if (filePath != null)
|
|
{
|
|
try
|
|
{
|
|
DataSerializer removed;
|
|
inProgressSerializations.TryRemove(filePath, out removed);
|
|
if (removed != null)
|
|
{
|
|
// Flush any contents to disk and remove the writer
|
|
removed.CloseStreams();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Do not care if there was an error removing this, must always delete if something failed
|
|
}
|
|
}
|
|
await requestContext.SendError(ex.Message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process request to save a resultSet to a file in CSV format
|
|
/// </summary>
|
|
internal Task HandleSerializeContinueRequest(SerializeDataContinueRequestParams serializeParams,
|
|
RequestContext<SerializeDataResult> requestContext)
|
|
{
|
|
// Run in separate thread so that message thread isn't held up by a potentially time consuming file write
|
|
Task.Run(async () =>
|
|
{
|
|
await RunSerializeContinueRequest(serializeParams, requestContext);
|
|
}).ContinueWithOnFaulted(async t => await SendErrorAndCleanup(serializeParams?.FilePath, requestContext, t.Exception));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
internal async Task RunSerializeContinueRequest(SerializeDataContinueRequestParams serializeParams, RequestContext<SerializeDataResult> requestContext)
|
|
{
|
|
try
|
|
{
|
|
// Verify we have sensible inputs and some data has already been sent for the file
|
|
Validate.IsNotNull(nameof(serializeParams), serializeParams);
|
|
Validate.IsNotNullOrWhitespaceString("FilePath", serializeParams.FilePath);
|
|
|
|
DataSerializer serializer = null;
|
|
if (!inProgressSerializations.TryGetValue(serializeParams.FilePath, out serializer))
|
|
{
|
|
throw new Exception(SR.SerializationServiceRequestNotFound(serializeParams.FilePath));
|
|
}
|
|
|
|
// Write to file and cleanup if needed
|
|
Logger.Write(TraceEventType.Verbose, "HandleSerializeContinueRequest");
|
|
SerializeDataResult result = serializer.ProcessRequest(serializeParams);
|
|
if (serializeParams.IsLastBatch)
|
|
{
|
|
// Cleanup the serializer
|
|
this.inProgressSerializations.TryRemove(serializer.FilePath, out serializer);
|
|
}
|
|
await requestContext.SendResult(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await SendErrorAndCleanup(serializeParams.FilePath, requestContext, ex);
|
|
}
|
|
}
|
|
}
|
|
class DataSerializer
|
|
{
|
|
private IFileStreamWriter writer;
|
|
private SerializeDataStartRequestParams requestParams;
|
|
private IReadOnlyList<DbColumnWrapper> columns;
|
|
|
|
public string FilePath { get; private set; }
|
|
|
|
public DataSerializer(SerializeDataStartRequestParams requestParams)
|
|
{
|
|
this.requestParams = requestParams;
|
|
this.columns = this.MapColumns(requestParams.Columns);
|
|
this.FilePath = requestParams.FilePath;
|
|
}
|
|
|
|
private IReadOnlyList<DbColumnWrapper> MapColumns(ColumnInfo[] columns)
|
|
{
|
|
List<DbColumnWrapper> columnWrappers = new List<DbColumnWrapper>();
|
|
foreach (ColumnInfo column in columns)
|
|
{
|
|
DbColumnWrapper wrapper = new DbColumnWrapper(column);
|
|
columnWrappers.Add(wrapper);
|
|
}
|
|
return columnWrappers;
|
|
}
|
|
|
|
|
|
public SerializeDataResult ProcessRequest(ISerializationParams serializeParams)
|
|
{
|
|
SerializeDataResult result = new SerializeDataResult();
|
|
try
|
|
{
|
|
this.WriteData(serializeParams.Rows, serializeParams.IsLastBatch);
|
|
if (serializeParams.IsLastBatch)
|
|
{
|
|
this.CloseStreams();
|
|
}
|
|
result.Succeeded = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Messages = ex.Message;
|
|
result.Succeeded = false;
|
|
this.CloseStreams();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public void WriteData(DbCellValue[][] rows, bool isComplete)
|
|
{
|
|
this.EnsureWriterCreated();
|
|
foreach (var row in rows)
|
|
{
|
|
SetRawObjects(row);
|
|
writer.WriteRow(row, this.columns);
|
|
}
|
|
}
|
|
|
|
private void SetRawObjects(DbCellValue[] row)
|
|
{
|
|
for (int i = 0; i < row.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
// Try to set as the "correct" type
|
|
var value = Convert.ChangeType(row[i].DisplayValue, columns[i].DataType);
|
|
row[i].RawObject = value;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
row[i].RawObject = row[i].DisplayValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EnsureWriterCreated()
|
|
{
|
|
if (this.writer == null)
|
|
{
|
|
IFileStreamFactory factory;
|
|
switch (this.requestParams.SaveFormat.ToLowerInvariant())
|
|
{
|
|
case "json":
|
|
factory = new SaveAsJsonFileStreamFactory()
|
|
{
|
|
SaveRequestParams = CreateJsonRequestParams()
|
|
};
|
|
break;
|
|
case "csv":
|
|
factory = new SaveAsCsvFileStreamFactory()
|
|
{
|
|
SaveRequestParams = CreateCsvRequestParams()
|
|
};
|
|
break;
|
|
case "markdown":
|
|
factory = new SaveAsMarkdownFileStreamFactory(CreateMarkdownRequestParams());
|
|
break;
|
|
case "xml":
|
|
factory = new SaveAsXmlFileStreamFactory()
|
|
{
|
|
SaveRequestParams = CreateXmlRequestParams()
|
|
};
|
|
break;
|
|
case "excel":
|
|
factory = new SaveAsExcelFileStreamFactory()
|
|
{
|
|
SaveRequestParams = CreateExcelRequestParams()
|
|
};
|
|
break;
|
|
default:
|
|
throw new Exception(SR.SerializationServiceUnsupportedFormat(this.requestParams.SaveFormat));
|
|
}
|
|
this.writer = factory.GetWriter(requestParams.FilePath, columns);
|
|
}
|
|
}
|
|
public void CloseStreams()
|
|
{
|
|
if (this.writer != null)
|
|
{
|
|
this.writer.Dispose();
|
|
this.writer = null;
|
|
}
|
|
}
|
|
|
|
private SaveResultsAsJsonRequestParams CreateJsonRequestParams()
|
|
{
|
|
return new SaveResultsAsJsonRequestParams
|
|
{
|
|
FilePath = this.requestParams.FilePath,
|
|
BatchIndex = 0,
|
|
ResultSetIndex = 0
|
|
};
|
|
}
|
|
private SaveResultsAsExcelRequestParams CreateExcelRequestParams()
|
|
{
|
|
return new SaveResultsAsExcelRequestParams
|
|
{
|
|
FilePath = this.requestParams.FilePath,
|
|
BatchIndex = 0,
|
|
ResultSetIndex = 0,
|
|
IncludeHeaders = this.requestParams.IncludeHeaders
|
|
};
|
|
}
|
|
private SaveResultsAsCsvRequestParams CreateCsvRequestParams()
|
|
{
|
|
return new SaveResultsAsCsvRequestParams
|
|
{
|
|
FilePath = this.requestParams.FilePath,
|
|
BatchIndex = 0,
|
|
ResultSetIndex = 0,
|
|
IncludeHeaders = this.requestParams.IncludeHeaders,
|
|
Delimiter = this.requestParams.Delimiter,
|
|
LineSeperator = this.requestParams.LineSeparator,
|
|
TextIdentifier = this.requestParams.TextIdentifier,
|
|
Encoding = this.requestParams.Encoding,
|
|
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
|
|
{
|
|
FilePath = this.requestParams.FilePath,
|
|
BatchIndex = 0,
|
|
ResultSetIndex = 0,
|
|
Formatted = this.requestParams.Formatted,
|
|
Encoding = this.requestParams.Encoding
|
|
};
|
|
}
|
|
}
|
|
} |