diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/GenerateDeployPlanRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/GenerateDeployPlanRequest.cs new file mode 100644 index 00000000..56d914bc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/Contracts/GenerateDeployPlanRequest.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx.Contracts +{ + /// + /// Parameters for a DacFx generate deploy plan request. + /// + public class GenerateDeployPlanParams : DacFxParams + { + } + + /// + /// Defines the DacFx generate deploy plan request type + /// + class GenerateDeployPlanRequest + { + public static readonly RequestType Type = + RequestType.Create("dacfx/generateDeployPlan"); + } + + /// + /// Parameters returned from a generate deploy script request. + /// + public class GenerateDeployPlanRequestResult : DacFxResult + { + /// + /// An xml string that details the alerts and the operations for deploying the specified dacpac to the database + /// + public string Report { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs index e8037aff..1fbc443b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxOperation.cs @@ -90,5 +90,23 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx } public abstract void Execute(); + + protected DacDeployOptions GetDefaultDeployOptions() + { + DacDeployOptions options = new DacDeployOptions + { + AllowDropBlockingAssemblies = true, + AllowIncompatiblePlatform = true, + BlockOnPossibleDataLoss = false, + DropObjectsNotInSource = true, + DropPermissionsNotInSource = true, + DropRoleMembersNotInSource = true, + IgnoreKeywordCasing = false, + IgnoreSemicolonBetweenStatements = false, + IgnoreWhitespace = false, + }; + + return options; + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs index 7ae9b2d4..874452d3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DacFxService.cs @@ -44,6 +44,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx serviceHost.SetRequestHandler(ExtractRequest.Type, this.HandleExtractRequest); serviceHost.SetRequestHandler(DeployRequest.Type, this.HandleDeployRequest); serviceHost.SetRequestHandler(GenerateDeployScriptRequest.Type, this.HandleGenerateDeployScriptRequest); + serviceHost.SetRequestHandler(GenerateDeployPlanRequest.Type, this.HandleGenerateDeployPlanRequest); } /// @@ -167,7 +168,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx // want to show filepath in task history instead of server and database metadata.ServerName = parameters.ScriptFilePath; - metadata.DatabaseName = ""; + metadata.DatabaseName = string.Empty; sqlTask = SqlTaskManagerInstance.CreateAndRun(metadata); @@ -175,7 +176,39 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx { OperationId = operation.OperationId, Success = true, - ErrorMessage = "" + ErrorMessage = string.Empty + }); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handles request to generate deploy plan + /// + /// + public async Task HandleGenerateDeployPlanRequest(GenerateDeployPlanParams parameters, RequestContext requestContext) + { + try + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) + { + GenerateDeployPlanOperation operation = new GenerateDeployPlanOperation(parameters, connInfo); + operation.Execute(parameters.TaskExecutionMode); + + await requestContext.SendResult(new GenerateDeployPlanRequestResult() + { + OperationId = operation.OperationId, + Success = true, + ErrorMessage = string.Empty, + Report = operation.DeployReport }); } } @@ -199,7 +232,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx { OperationId = operation.OperationId, Success = true, - ErrorMessage = "" + ErrorMessage = string.Empty }); } diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs index f6d07bba..50672b0a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/DeployOperation.cs @@ -29,7 +29,8 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx public override void Execute() { DacPackage dacpac = DacPackage.Load(this.Parameters.PackageFilePath); - this.DacServices.Deploy(dacpac, this.Parameters.DatabaseName, this.Parameters.UpgradeExisting, null, this.CancellationToken); + DacDeployOptions options = this.GetDefaultDeployOptions(); + this.DacServices.Deploy(dacpac, this.Parameters.DatabaseName, this.Parameters.UpgradeExisting, options, this.CancellationToken); } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployPlanOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployPlanOperation.cs new file mode 100644 index 00000000..e72bf8d9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployPlanOperation.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using Microsoft.SqlServer.Dac; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.Diagnostics; + +namespace Microsoft.SqlTools.ServiceLayer.DacFx +{ + /// + /// Class to represent an in-progress generate deploy plan operation + /// + class GenerateDeployPlanOperation : DacFxOperation + { + public GenerateDeployPlanParams Parameters { get; } + + public string DeployReport { get; set; } + + public GenerateDeployPlanOperation(GenerateDeployPlanParams parameters, ConnectionInfo connInfo): base(connInfo) + { + Validate.IsNotNull("parameters", parameters); + this.Parameters = parameters; + } + + public override void Execute() + { + DacPackage dacpac = DacPackage.Load(this.Parameters.PackageFilePath); + DacDeployOptions options = GetDefaultDeployOptions(); + DeployReport = this.DacServices.GenerateDeployReport(dacpac, this.Parameters.DatabaseName, options, this.CancellationToken); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployScriptOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployScriptOperation.cs index 0ef7bee1..d68cab29 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployScriptOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DacFx/GenerateDeployScriptOperation.cs @@ -33,6 +33,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DacFx PublishOptions publishOptions = new PublishOptions(); publishOptions.GenerateDeploymentReport = this.Parameters.GenerateDeploymentReport; publishOptions.CancelToken = this.CancellationToken; + publishOptions.DeployOptions = this.GetDefaultDeployOptions(); publishOptions.DatabaseScriptPath = this.Parameters.ScriptFilePath; // master script is only used if the target is Azure SQL db and the script contains all operations that must be done against the master database publishOptions.MasterDbScriptPath = Path.Combine(Path.GetDirectoryName(this.Parameters.ScriptFilePath), string.Concat("master_", Path.GetFileName(this.Parameters.ScriptFilePath))); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs index d97ce325..33d5dcad 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DacFx/DacFxserviceTests.cs @@ -199,7 +199,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DacFx { service.PerformOperation(operation); } - catch(OperationCanceledException canceledException) + catch (OperationCanceledException canceledException) { expectedException = canceledException; } @@ -260,6 +260,74 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DacFx return generateScriptRequestContext; } + private async Task>> SendAndValidateGenerateDeployPlanRequest() + { + var result = GetLiveAutoCompleteTestObjects(); + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxGenerateDeployPlanTest"); + sourceDb.RunQuery(@"CREATE TABLE [dbo].[table1] +( + [ID] INT NOT NULL PRIMARY KEY, + [Date] DATE NOT NULL +) +CREATE TABLE [dbo].[table2] +( + [ID] INT NOT NULL PRIMARY KEY, + [col1] NCHAR(10) NULL +)"); + DacFxService service = new DacFxService(); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DacFxTest"); + Directory.CreateDirectory(folderPath); + + var extractParams = new ExtractParams + { + DatabaseName = sourceDb.DatabaseName, + PackageFilePath = Path.Combine(folderPath, string.Format("{0}.dacpac", sourceDb.DatabaseName)), + ApplicationName = "test", + ApplicationVersion = new Version(1, 0) + }; + + ExtractOperation extractOperation = new ExtractOperation(extractParams, result.ConnectionInfo); + service.PerformOperation(extractOperation); + + // generate deploy plan for deploying dacpac to targetDb + var generateDeployPlanRequestContext = new Mock>(); + generateDeployPlanRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + + SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "DacFxGenerateDeployPlanTestTarget"); + targetDb.RunQuery(@"CREATE TABLE [dbo].[table2] +( + [ID] INT NOT NULL PRIMARY KEY, + [col1] NCHAR(10) NULL, + [col2] NCHAR(10) NULL +) +CREATE TABLE [dbo].[table3] +( + [ID] INT NOT NULL PRIMARY KEY, + [col1] INT NULL, +)"); + + var generateDeployPlanParams = new GenerateDeployPlanParams + { + PackageFilePath = extractParams.PackageFilePath, + DatabaseName = targetDb.DatabaseName, + }; + + GenerateDeployPlanOperation generateDeployPlanOperation = new GenerateDeployPlanOperation(generateDeployPlanParams, result.ConnectionInfo); + service.PerformOperation(generateDeployPlanOperation); + string report = generateDeployPlanOperation.DeployReport; + Assert.NotNull(report); + Assert.Contains("Create", report); + Assert.Contains("Drop", report); + Assert.Contains("Alter", report); + + // cleanup + VerifyAndCleanup(extractParams.PackageFilePath); + sourceDb.Cleanup(); + targetDb.Cleanup(); + + return generateDeployPlanRequestContext; + } + /// /// Verify the export bacpac request /// @@ -306,7 +374,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DacFx } /// - /// Verify the gnerate deploy script request + /// Verify the generate deploy script request /// [Fact] public async void GenerateDeployScript() @@ -314,6 +382,15 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DacFx Assert.NotNull(await SendAndValidateGenerateDeployScriptRequest()); } + /// + /// Verify the generate deploy plan request + /// + [Fact] + public async void GenerateDeployPlan() + { + Assert.NotNull(await SendAndValidateGenerateDeployPlanRequest()); + } + private void VerifyAndCleanup(string filePath) { // Verify it was created