diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs index 78c2c72f..721d13c9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs @@ -18,16 +18,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public string FilePath { get; set; } - /// - /// The encoding of the file to save results in - /// - public string FileEncoding { get; set; } - - /// - /// Include headers of columns in CSV - /// - public bool IncludeHeaders { get; set; } - /// /// Index of the batch to get the results from /// @@ -38,15 +28,38 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public int ResultSetIndex { get; set; } + /// + /// URI for the editor that called save results + /// + public string OwnerUri { get; set; } + } + + /// + /// Parameters to save results as CSV + /// + public class SaveResultsAsCsvRequestParams: SaveResultsRequestParams{ + /// /// CSV - Write values in quotes /// public Boolean ValueInQuotes { get; set; } /// - /// URI for the editor that called save results + /// The encoding of the file to save results in /// - public string OwnerUri { get; set; } + public string FileEncoding { get; set; } + + /// + /// Include headers of columns in CSV + /// + public bool IncludeHeaders { get; set; } + } + + /// + /// Parameters to save results as JSON + /// + public class SaveResultsAsJsonRequestParams: SaveResultsRequestParams{ + //TODO: define config for save as JSON } /// @@ -60,11 +73,24 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts public string Messages { get; set; } } + /// + /// Request type to save results as CSV + /// public class SaveResultsAsCsvRequest { public static readonly - RequestType Type = - RequestType.Create("query/save"); + RequestType Type = + RequestType.Create("query/saveCsv"); + } + + /// + /// Request type to save results as JSON + /// + public class SaveResultsAsJsonRequest + { + public static readonly + RequestType Type = + RequestType.Create("query/saveJson"); } } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index 882cf11b..24d4de09 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -14,6 +14,7 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; +using Newtonsoft.Json; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { @@ -100,6 +101,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution serviceHost.SetRequestHandler(QueryDisposeRequest.Type, HandleDisposeRequest); serviceHost.SetRequestHandler(QueryCancelRequest.Type, HandleCancelRequest); serviceHost.SetRequestHandler(SaveResultsAsCsvRequest.Type, HandleSaveResultsAsCsvRequest); + serviceHost.SetRequestHandler(SaveResultsAsJsonRequest.Type, HandleSaveResultsAsJsonRequest); // Register handler for shutdown event serviceHost.RegisterShutdownTask((shutdownParams, requestContext) => @@ -261,7 +263,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// /// Process request to save a resultSet to a file in CSV format /// - public async Task HandleSaveResultsAsCsvRequest( SaveResultsRequestParams saveParams, + public async Task HandleSaveResultsAsCsvRequest( SaveResultsAsCsvRequestParams saveParams, RequestContext requestContext) { // retrieve query for OwnerUri @@ -276,28 +278,94 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } try { - using (StreamWriter csvFile = new StreamWriter(File.OpenWrite(saveParams.FilePath))) + using (StreamWriter csvFile = new StreamWriter(File.Open(saveParams.FilePath, FileMode.Create))) { // get the requested resultSet from query Batch selectedBatch = result.Batches[saveParams.BatchIndex]; ResultSet selectedResultSet = (selectedBatch.ResultSets.ToList())[saveParams.ResultSetIndex]; - if ( saveParams.IncludeHeaders) + if (saveParams.IncludeHeaders) { // write column names to csv await csvFile.WriteLineAsync( string.Join( ",", selectedResultSet.Columns.Select( column => SaveResults.EncodeCsvField(column.ColumnName) ?? string.Empty))); } // write rows to csv - foreach( var row in selectedResultSet.Rows) + foreach (var row in selectedResultSet.Rows) { - await csvFile.WriteLineAsync(string.Join( ",", row.Select( field => SaveResults.EncodeCsvField( (field != null) ? field.ToString(): string.Empty)))); + await csvFile.WriteLineAsync( string.Join( ",", row.Select( field => SaveResults.EncodeCsvField((field != null) ? field.ToString(): string.Empty)))); } } } catch(Exception ex) { // Delete file when exception occurs - if(File.Exists(saveParams.FilePath)) + if (File.Exists(saveParams.FilePath)) + { + File.Delete(saveParams.FilePath); + } + await requestContext.SendError(ex.Message); + return; + } + await requestContext.SendResult(new SaveResultRequestResult + { + Messages = "Success" + }); + return; + } + + /// + /// Process request to save a resultSet to a file in JSON format + /// + public async Task HandleSaveResultsAsJsonRequest( SaveResultsAsJsonRequestParams saveParams, + RequestContext requestContext) + { + // retrieve query for OwnerUri + Query result; + if (!ActiveQueries.TryGetValue(saveParams.OwnerUri, out result)) + { + await requestContext.SendResult(new SaveResultRequestResult + { + Messages = "Failed to save results, ID not found." + }); + return; + } + try + { + using (StreamWriter jsonFile = new StreamWriter(File.Open(saveParams.FilePath, FileMode.Create))) + using (JsonWriter jsonWriter = new JsonTextWriter(jsonFile) ) + { + jsonWriter.Formatting = Formatting.Indented; + jsonWriter.WriteStartArray(); + + // get the requested resultSet from query + Batch selectedBatch = result.Batches[saveParams.BatchIndex]; + ResultSet selectedResultSet = (selectedBatch.ResultSets.ToList())[saveParams.ResultSetIndex]; + + // write each row to JSON + foreach (var row in selectedResultSet.Rows) + { + jsonWriter.WriteStartObject(); + foreach (var field in row.Select((value,i) => new {value, i})) + { + jsonWriter.WritePropertyName(selectedResultSet.Columns[field.i].ColumnName); + if (field.value != null) + { + jsonWriter.WriteValue(field.value); + } + else + { + jsonWriter.WriteNull(); + } + } + jsonWriter.WriteEndObject(); + } + jsonWriter.WriteEndArray(); + } + } + catch(Exception ex) + { + // Delete file when exception occurs + if (File.Exists(saveParams.FilePath)) { File.Delete(saveParams.FilePath); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SaveResultsTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SaveResultsTests.cs index 61cf5fea..b359705a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SaveResultsTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/QueryExecution/SaveResultsTests.cs @@ -32,7 +32,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); // Request to save the results as csv with correct parameters - var saveParams = new SaveResultsRequestParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + var saveParams = new SaveResultsAsCsvRequestParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; saveParams.FilePath = "testwrite.csv"; saveParams.IncludeHeaders = true; SaveResultRequestResult result = null; @@ -46,14 +46,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution VerifySaveResultsCallCount(saveRequest, Times.Once(), Times.Never()); // Delete temp file after test - if(File.Exists(saveParams.FilePath)) + if (File.Exists(saveParams.FilePath)) { File.Delete(saveParams.FilePath); } } /// - /// Test handling exception in saving results to file + /// Test handling exception in saving results to CSV file /// [Fact] public void SaveResultsAsCsvExceptionTest() @@ -65,8 +65,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); // Request to save the results as csv with incorrect filepath - var saveParams = new SaveResultsRequestParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; - if ( RuntimeInformation.IsOSPlatform( OSPlatform.Windows)) + var saveParams = new SaveResultsAsCsvRequestParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + if (RuntimeInformation.IsOSPlatform( OSPlatform.Windows)) { saveParams.FilePath = "G:\\test.csv"; } @@ -75,8 +75,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution saveParams.FilePath = "/test.csv"; } // SaveResultRequestResult result = null; - String errMessage = null; - var saveRequest = GetSaveResultsContextMock( null, err => errMessage = (String) err); + string errMessage = null; + var saveRequest = GetSaveResultsContextMock( null, err => errMessage = (string) err); queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch(); queryService.HandleSaveResultsAsCsvRequest(saveParams, saveRequest.Object).Wait(); @@ -87,7 +87,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution } /// - /// Test saving results to file when the requested result set is no longer active + /// Test saving results to CSV file when the requested result set is no longer active /// [Fact] public void SaveResultsAsCsvQueryNotFoundTest() @@ -99,11 +99,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); // Request to save the results as csv with query that is no longer active - var saveParams = new SaveResultsRequestParams { OwnerUri = "falseuri", ResultSetIndex = 0, BatchIndex = 0 }; + var saveParams = new SaveResultsAsCsvRequestParams { OwnerUri = "falseuri", ResultSetIndex = 0, BatchIndex = 0 }; saveParams.FilePath = "testwrite.csv"; SaveResultRequestResult result = null; var saveRequest = GetSaveResultsContextMock(qcr => result = qcr, null); - // queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch(); queryService.HandleSaveResultsAsCsvRequest(saveParams, saveRequest.Object).Wait(); // Expect message that save failed @@ -112,6 +111,97 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.QueryExecution VerifySaveResultsCallCount(saveRequest, Times.Once(), Times.Never()); } + /// + /// Test save results to a file as JSON with correct parameters + /// + [Fact] + public void SaveResultsAsJsonSuccessTest() + { + // Execute a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri }; + var executeRequest = GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // Request to save the results as json with correct parameters + var saveParams = new SaveResultsAsJsonRequestParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + saveParams.FilePath = "testwrite.json"; + SaveResultRequestResult result = null; + var saveRequest = GetSaveResultsContextMock(qcr => result = qcr, null); + queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch(); + queryService.HandleSaveResultsAsJsonRequest(saveParams, saveRequest.Object).Wait(); + + // Expect to see a file successfully created in filepath and a success message + Assert.Equal("Success", result.Messages); + Assert.True(File.Exists(saveParams.FilePath)); + VerifySaveResultsCallCount(saveRequest, Times.Once(), Times.Never()); + + // Delete temp file after test + if (File.Exists(saveParams.FilePath)) + { + File.Delete(saveParams.FilePath); + } + } + + /// + /// Test handling exception in saving results to JSON file + /// + [Fact] + public void SaveResultsAsJsonExceptionTest() + { + // Execute a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri }; + var executeRequest = GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // Request to save the results as json with incorrect filepath + var saveParams = new SaveResultsAsJsonRequestParams { OwnerUri = Common.OwnerUri, ResultSetIndex = 0, BatchIndex = 0 }; + if (RuntimeInformation.IsOSPlatform( OSPlatform.Windows)) + { + saveParams.FilePath = "G:\\test.json"; + } + else + { + saveParams.FilePath = "/test.json"; + } + // SaveResultRequestResult result = null; + string errMessage = null; + var saveRequest = GetSaveResultsContextMock( null, err => errMessage = (string) err); + queryService.ActiveQueries[Common.OwnerUri].Batches[0] = Common.GetBasicExecutedBatch(); + queryService.HandleSaveResultsAsJsonRequest(saveParams, saveRequest.Object).Wait(); + + // Expect to see error message + Assert.NotNull(errMessage); + VerifySaveResultsCallCount(saveRequest, Times.Never(), Times.Once()); + Assert.False(File.Exists(saveParams.FilePath)); + } + + /// + /// Test saving results to JSON file when the requested result set is no longer active + /// + [Fact] + public void SaveResultsAsJsonQueryNotFoundTest() + { + // Execute a query + var queryService = Common.GetPrimedExecutionService(Common.CreateMockFactory(null, false), true); + var executeParams = new QueryExecuteParams { QueryText = Common.StandardQuery, OwnerUri = Common.OwnerUri }; + var executeRequest = GetQueryExecuteResultContextMock(null, null, null); + queryService.HandleExecuteRequest(executeParams, executeRequest.Object).Wait(); + + // Request to save the results as json with query that is no longer active + var saveParams = new SaveResultsAsJsonRequestParams { OwnerUri = "falseuri", ResultSetIndex = 0, BatchIndex = 0 }; + saveParams.FilePath = "testwrite.json"; + SaveResultRequestResult result = null; + var saveRequest = GetSaveResultsContextMock(qcr => result = qcr, null); + queryService.HandleSaveResultsAsJsonRequest(saveParams, saveRequest.Object).Wait(); + + // Expect message that save failed + Assert.Equal("Failed to save results, ID not found.", result.Messages); + Assert.False(File.Exists(saveParams.FilePath)); + VerifySaveResultsCallCount(saveRequest, Times.Once(), Times.Never()); + } + #region Mocking ///