From 432e054d55597b7052f8f21099ee14663c44448c Mon Sep 17 00:00:00 2001 From: udeeshagautam <46980425+udeeshagautam@users.noreply.github.com> Date: Thu, 13 Jun 2019 17:28:59 -0700 Subject: [PATCH] Feature/schemacompare scmp save (#824) * First cut of scmp Save related changes and some test refactoring * Adding Exclude/Include objects in saving * Add diff entry validation as part of test * Adding PR comments - major change is change to nameparts in place of name hence preserving any "."/"[" in name and avoiding any string operations * One more UT scenario addition for create excluded object --- .../Contracts/SchemaCompareRequest.cs | 6 +- .../Contracts/SchemaCompareSaveScmpRequest.cs | 35 ++ ...chemaCompareIncludeExcludeNodeOperation.cs | 4 +- .../SchemaCompare/SchemaCompareOperation.cs | 136 +------ .../SchemaCompareSaveScmpOperation.cs | 120 ++++++ .../SchemaCompare/SchemaCompareService.cs | 67 ++- .../SchemaCompare/SchemaCompareUtils.cs | 163 ++++++++ .../SchemaCompareServiceOptionsTests.cs | 81 +--- .../SchemaCompareServiceTests.cs | 382 ++++++++++++++++-- .../SchemaCompare/SchemaCompareTestUtils.cs | 61 +++ .../SchemaCompare/SchemaCompareTests.cs | 63 ++- 11 files changed, 883 insertions(+), 235 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareSaveScmpRequest.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareSaveScmpOperation.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs index 1431e9f1..9a031391 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs @@ -88,12 +88,14 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts public SchemaUpdateAction UpdateAction { get; set; } public SchemaDifferenceType DifferenceType { get; set; } public string Name { get; set; } - public string SourceValue { get; set; } - public string TargetValue { get; set; } + public string[] SourceValue { get; set; } + public string[] TargetValue { get; set; } public DiffEntry Parent { get; set; } public List Children { get; set; } public string SourceScript { get; set; } public string TargetScript { get; set; } + public string SourceObjectType { get; set; } + public string TargetObjectType { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareSaveScmpRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareSaveScmpRequest.cs new file mode 100644 index 00000000..42b82da0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareSaveScmpRequest.cs @@ -0,0 +1,35 @@ +// +// 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.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts +{ + internal class SchemaCompareSaveScmpParams : SchemaCompareParams + { + /// + /// Gets or sets the File Path for scmp + /// + public string ScmpFilePath { get; set; } + + /// + /// Excluded source objects + /// + public SchemaCompareObjectId[] ExcludedSourceObjects { get; set; } + + /// + /// Excluded Target objects + /// + public SchemaCompareObjectId[] ExcludedTargetObjects { get; set; } + } + + internal class SchemaCompareSaveScmpRequest + { + public static readonly RequestType Type = + RequestType.Create("schemaCompare/saveScmp"); + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs index c890c43c..1e244c8e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs @@ -9,8 +9,6 @@ using Microsoft.SqlTools.Utility; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; using System.Threading; namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare @@ -94,7 +92,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare { bool result = true; // Create a diff entry from difference and check if it matches the diff entry passed - DiffEntry entryFromDifference = SchemaCompareOperation.CreateDiffEntry(difference, null); + DiffEntry entryFromDifference = SchemaCompareUtils.CreateDiffEntry(difference, null); System.Reflection.PropertyInfo[] properties = diffEntry.GetType().GetProperties(); foreach (var prop in properties) diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareOperation.cs index 1a7858f1..f17d4e7f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareOperation.cs @@ -2,7 +2,6 @@ // 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.SqlServer.Dac.Compare; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; @@ -11,7 +10,6 @@ using Microsoft.SqlTools.Utility; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Text.RegularExpressions; using System.Threading; namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare @@ -45,8 +43,8 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare { Validate.IsNotNull("parameters", parameters); this.Parameters = parameters; - this.SourceConnectionString = GetConnectionString(sourceConnInfo, parameters.SourceEndpointInfo.DatabaseName); - this.TargetConnectionString = GetConnectionString(targetConnInfo, parameters.TargetEndpointInfo.DatabaseName); + this.SourceConnectionString = SchemaCompareUtils.GetConnectionString(sourceConnInfo, parameters.SourceEndpointInfo.DatabaseName); + this.TargetConnectionString = SchemaCompareUtils.GetConnectionString(targetConnInfo, parameters.TargetEndpointInfo.DatabaseName); this.OperationId = Guid.NewGuid().ToString(); } @@ -83,14 +81,14 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare try { - SchemaCompareEndpoint sourceEndpoint = CreateSchemaCompareEndpoint(this.Parameters.SourceEndpointInfo, this.SourceConnectionString); - SchemaCompareEndpoint targetEndpoint = CreateSchemaCompareEndpoint(this.Parameters.TargetEndpointInfo, this.TargetConnectionString); + SchemaCompareEndpoint sourceEndpoint = SchemaCompareUtils.CreateSchemaCompareEndpoint(this.Parameters.SourceEndpointInfo, this.SourceConnectionString); + SchemaCompareEndpoint targetEndpoint = SchemaCompareUtils.CreateSchemaCompareEndpoint(this.Parameters.TargetEndpointInfo, this.TargetConnectionString); SchemaComparison comparison = new SchemaComparison(sourceEndpoint, targetEndpoint); if (this.Parameters.DeploymentOptions != null) { - comparison.Options = this.CreateSchemaCompareOptions(this.Parameters.DeploymentOptions); + comparison.Options = SchemaCompareUtils.CreateSchemaCompareOptions(this.Parameters.DeploymentOptions); } this.ComparisonResult = comparison.Compare(); @@ -104,7 +102,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare this.Differences = new List(); foreach (SchemaDifference difference in this.ComparisonResult.Differences) { - DiffEntry diffEntry = CreateDiffEntry(difference, null); + DiffEntry diffEntry = SchemaCompareUtils.CreateDiffEntry(difference, null); this.Differences.Add(diffEntry); } } @@ -115,127 +113,5 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare throw; } } - - private DacDeployOptions CreateSchemaCompareOptions(DeploymentOptions deploymentOptions) - { - System.Reflection.PropertyInfo[] deploymentOptionsProperties = deploymentOptions.GetType().GetProperties(); - - DacDeployOptions dacOptions = new DacDeployOptions(); - foreach (var deployOptionsProp in deploymentOptionsProperties) - { - var prop = dacOptions.GetType().GetProperty(deployOptionsProp.Name); - if (prop != null) - { - prop.SetValue(dacOptions, deployOptionsProp.GetValue(deploymentOptions)); - } - } - return dacOptions; - } - - internal static DiffEntry CreateDiffEntry(SchemaDifference difference, DiffEntry parent) - { - if (difference == null) - { - return null; - } - - DiffEntry diffEntry = new DiffEntry(); - diffEntry.UpdateAction = difference.UpdateAction; - diffEntry.DifferenceType = difference.DifferenceType; - diffEntry.Name = difference.Name; - - if (difference.SourceObject != null) - { - diffEntry.SourceValue = GetName(difference.SourceObject.Name.ToString()); - } - if (difference.TargetObject != null) - { - diffEntry.TargetValue = GetName(difference.TargetObject.Name.ToString()); - } - - if (difference.DifferenceType == SchemaDifferenceType.Object) - { - // set source and target scripts - if (difference.SourceObject != null) - { - string sourceScript; - difference.SourceObject.TryGetScript(out sourceScript); - diffEntry.SourceScript = FormatScript(sourceScript); - } - if (difference.TargetObject != null) - { - string targetScript; - difference.TargetObject.TryGetScript(out targetScript); - diffEntry.TargetScript = FormatScript(targetScript); - } - } - - diffEntry.Children = new List(); - - foreach (SchemaDifference child in difference.Children) - { - diffEntry.Children.Add(CreateDiffEntry(child, diffEntry)); - } - - return diffEntry; - } - - private SchemaCompareEndpoint CreateSchemaCompareEndpoint(SchemaCompareEndpointInfo endpointInfo, string connectionString) - { - switch (endpointInfo.EndpointType) - { - case SchemaCompareEndpointType.Dacpac: - { - return new SchemaCompareDacpacEndpoint(endpointInfo.PackageFilePath); - } - case SchemaCompareEndpointType.Database: - { - return new SchemaCompareDatabaseEndpoint(connectionString); - } - default: - { - return null; - } - } - } - - private string GetConnectionString(ConnectionInfo connInfo, string databaseName) - { - if (connInfo == null) - { - return null; - } - - connInfo.ConnectionDetails.DatabaseName = databaseName; - return ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); - } - - public static string RemoveExcessWhitespace(string script) - { - if (script != null) - { - // remove leading and trailing whitespace - script = script.Trim(); - // replace all multiple spaces with single space - script = Regex.Replace(script, " {2,}", " "); - } - return script; - } - - public static string FormatScript(string script) - { - script = RemoveExcessWhitespace(script); - if (!string.IsNullOrWhiteSpace(script) && !script.Equals("null")) - { - script += Environment.NewLine + "GO"; - } - return script; - } - - private static string GetName(string name) - { - // remove brackets from name - return Regex.Replace(name, @"[\[\]]", ""); - } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareSaveScmpOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareSaveScmpOperation.cs new file mode 100644 index 00000000..76891ac4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareSaveScmpOperation.cs @@ -0,0 +1,120 @@ +// +// 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.Compare; +using Microsoft.SqlServer.Dac.Model; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; +using System; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare +{ + class SchemaCompareSaveScmpOperation : ITaskOperation + { + private CancellationTokenSource cancellation = new CancellationTokenSource(); + private bool disposed = false; + + /// + /// Gets the unique id associated with this instance. + /// + public string OperationId { get; private set; } + + protected CancellationToken CancellationToken { get { return this.cancellation.Token; } } + + public string ErrorMessage { get; set; } + + public SqlTask SqlTask { get; set; } + + public SchemaCompareSaveScmpParams Parameters { get; set; } + + public string SourceConnectionString { get; set; } + + public string TargetConnectionString { get; set; } + + public SchemaCompareSaveScmpOperation(SchemaCompareSaveScmpParams parameters, ConnectionInfo sourceConnInfo, ConnectionInfo targetConnInfo) + { + Validate.IsNotNull("parameters", parameters); + Validate.IsNotNull("parameters.ScmpFilePath", parameters.ScmpFilePath); + this.Parameters = parameters; + this.SourceConnectionString = SchemaCompareUtils.GetConnectionString(sourceConnInfo, parameters.SourceEndpointInfo.DatabaseName); + this.TargetConnectionString = SchemaCompareUtils.GetConnectionString(targetConnInfo, parameters.TargetEndpointInfo.DatabaseName); + this.OperationId = Guid.NewGuid().ToString(); + } + + public void Execute(TaskExecutionMode mode = TaskExecutionMode.Execute) + { + if (this.CancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(this.CancellationToken); + } + + try + { + SchemaCompareEndpoint sourceEndpoint = SchemaCompareUtils.CreateSchemaCompareEndpoint(this.Parameters.SourceEndpointInfo, this.SourceConnectionString); + SchemaCompareEndpoint targetEndpoint = SchemaCompareUtils.CreateSchemaCompareEndpoint(this.Parameters.TargetEndpointInfo, this.TargetConnectionString); + + SchemaComparison comparison = new SchemaComparison(sourceEndpoint, targetEndpoint); + + if (Parameters.ExcludedSourceObjects != null) + { + foreach (var sourceObj in this.Parameters.ExcludedSourceObjects) + { + SchemaComparisonExcludedObjectId excludedObjId = SchemaCompareUtils.CreateExcludedObject(sourceObj); + if (excludedObjId != null) + { + comparison.ExcludedSourceObjects.Add(excludedObjId); + } + } + } + + if (Parameters.ExcludedTargetObjects != null) + { + foreach (var targetObj in this.Parameters.ExcludedTargetObjects) + { + SchemaComparisonExcludedObjectId excludedObjId = SchemaCompareUtils.CreateExcludedObject(targetObj); + if (excludedObjId != null) + { + comparison.ExcludedTargetObjects.Add(excludedObjId); + } + } + } + + if (this.Parameters.DeploymentOptions != null) + { + comparison.Options = SchemaCompareUtils.CreateSchemaCompareOptions(this.Parameters.DeploymentOptions); + } + + comparison.SaveToFile(this.Parameters.ScmpFilePath, true); + + } + catch (Exception e) + { + ErrorMessage = e.Message; + Logger.Write(TraceEventType.Error, string.Format("Schema compare save settings operation {0} failed with exception {1}", this.OperationId, e)); + throw; + } + } + + // The schema compare public api doesn't currently take a cancellation token for scmp save so the operation can't be cancelled + public void Cancel() + { + } + + /// + /// Disposes the operation. + /// + public void Dispose() + { + if (!disposed) + { + this.Cancel(); + disposed = true; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs index a9568563..25a77151 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs @@ -12,6 +12,8 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using Microsoft.SqlServer.Dac.Compare; using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; +using System.Diagnostics; namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare { @@ -49,10 +51,9 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare serviceHost.SetRequestHandler(SchemaCompareIncludeExcludeNodeRequest.Type, this.HandleSchemaCompareIncludeExcludeNodeRequest); serviceHost.SetRequestHandler(SchemaCompareGetDefaultOptionsRequest.Type, this.HandleSchemaCompareGetDefaultOptionsRequest); serviceHost.SetRequestHandler(SchemaCompareOpenScmpRequest.Type, this.HandleSchemaCompareOpenScmpRequest); + serviceHost.SetRequestHandler(SchemaCompareSaveScmpRequest.Type, this.HandleSchemaCompareSaveScmpRequest); } - - /// /// Handles schema compare request /// @@ -70,7 +71,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare parameters.TargetEndpointInfo.OwnerUri, out targetConnInfo); - Task schemaCompareTask = Task.Run(async () => + CurrentSchemaCompareTask = Task.Run(async () => { SchemaCompareOperation operation = null; @@ -93,6 +94,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } catch (Exception e) { + Logger.Write(TraceEventType.Error, "Failed to compare schema. Error: " + e); await requestContext.SendResult(new SchemaCompareResult() { OperationId = operation != null ? operation.OperationId : null, @@ -137,11 +139,12 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } catch (Exception e) { + Logger.Write(TraceEventType.Error, "Failed to generate schema compare script. Error: " + e); await requestContext.SendResult(new ResultStatus() { Success = false, ErrorMessage = operation == null ? e.Message : operation.ErrorMessage, - }); + }); } } @@ -173,6 +176,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } catch (Exception e) { + Logger.Write(TraceEventType.Error, "Failed to publish schema compare changes. Error: " + e); await requestContext.SendResult(new ResultStatus() { Success = false, @@ -181,6 +185,10 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } } + /// + /// Handles request for exclude incude node in Schema compare result + /// + /// public async Task HandleSchemaCompareIncludeExcludeNodeRequest(SchemaCompareNodeParams parameters, RequestContext requestContext) { SchemaCompareIncludeExcludeNodeOperation operation = null; @@ -199,6 +207,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } catch (Exception e) { + Logger.Write(TraceEventType.Error, "Failed to select compare schema result node. Error: " + e); await requestContext.SendResult(new ResultStatus() { Success = false, @@ -207,6 +216,10 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } } + /// + /// Handles request to create default deployment options as per DacFx + /// + /// public async Task HandleSchemaCompareGetDefaultOptionsRequest(SchemaCompareGetOptionsParams parameters, RequestContext requestContext) { try @@ -267,6 +280,52 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare } } + /// + /// Handles schema compare save SCMP request + /// + /// + public async Task HandleSchemaCompareSaveScmpRequest(SchemaCompareSaveScmpParams parameters, RequestContext requestContext) + { + try + { + ConnectionInfo sourceConnInfo; + ConnectionInfo targetConnInfo; + ConnectionServiceInstance.TryFindConnection(parameters.SourceEndpointInfo.OwnerUri, out sourceConnInfo); + ConnectionServiceInstance.TryFindConnection(parameters.TargetEndpointInfo.OwnerUri, out targetConnInfo); + + CurrentSchemaCompareTask = Task.Run(async () => + { + SchemaCompareSaveScmpOperation operation = null; + + try + { + operation = new SchemaCompareSaveScmpOperation(parameters, sourceConnInfo, targetConnInfo); + operation.Execute(parameters.TaskExecutionMode); + + await requestContext.SendResult(new ResultStatus() + { + Success = true, + ErrorMessage = operation.ErrorMessage, + }); + } + catch (Exception e) + { + Logger.Write(TraceEventType.Error, "Failed to save scmp file. Error: " + e); + await requestContext.SendResult(new SchemaCompareResult() + { + OperationId = operation != null ? operation.OperationId : null, + Success = false, + ErrorMessage = operation == null ? e.Message : operation.ErrorMessage, + }); + } + }); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + private SqlTaskManager SqlTaskManagerInstance { get diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs new file mode 100644 index 00000000..b4d6538d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs @@ -0,0 +1,163 @@ +// +// 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.SqlServer.Dac.Compare; +using Microsoft.SqlServer.Dac.Model; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare +{ + + /// + /// Internal class for utilities shared between multiple schema compare operations + /// + internal static class SchemaCompareUtils + { + internal static DacDeployOptions CreateSchemaCompareOptions(DeploymentOptions deploymentOptions) + { + System.Reflection.PropertyInfo[] deploymentOptionsProperties = deploymentOptions.GetType().GetProperties(); + + DacDeployOptions dacOptions = new DacDeployOptions(); + foreach (var deployOptionsProp in deploymentOptionsProperties) + { + var prop = dacOptions.GetType().GetProperty(deployOptionsProp.Name); + if (prop != null) + { + prop.SetValue(dacOptions, deployOptionsProp.GetValue(deploymentOptions)); + } + } + return dacOptions; + } + + internal static DiffEntry CreateDiffEntry(SchemaDifference difference, DiffEntry parent) + { + if (difference == null) + { + return null; + } + + DiffEntry diffEntry = new DiffEntry(); + diffEntry.UpdateAction = difference.UpdateAction; + diffEntry.DifferenceType = difference.DifferenceType; + diffEntry.Name = difference.Name; + + if (difference.SourceObject != null) + { + diffEntry.SourceValue = difference.SourceObject.Name.Parts.ToArray(); + var sourceType = new SchemaComparisonExcludedObjectId(difference.SourceObject.ObjectType, difference.SourceObject.Name); + diffEntry.SourceObjectType = sourceType.TypeName; + + } + if (difference.TargetObject != null) + { + diffEntry.TargetValue = difference.TargetObject.Name.Parts.ToArray(); + var targetType = new SchemaComparisonExcludedObjectId(difference.TargetObject.ObjectType, difference.TargetObject.Name); + diffEntry.TargetObjectType = targetType.TypeName; + } + + if (difference.DifferenceType == SchemaDifferenceType.Object) + { + // set source and target scripts + if (difference.SourceObject != null) + { + string sourceScript; + difference.SourceObject.TryGetScript(out sourceScript); + diffEntry.SourceScript = FormatScript(sourceScript); + } + if (difference.TargetObject != null) + { + string targetScript; + difference.TargetObject.TryGetScript(out targetScript); + diffEntry.TargetScript = FormatScript(targetScript); + } + } + + diffEntry.Children = new List(); + + foreach (SchemaDifference child in difference.Children) + { + diffEntry.Children.Add(CreateDiffEntry(child, diffEntry)); + } + + return diffEntry; + } + + internal static SchemaComparisonExcludedObjectId CreateExcludedObject(SchemaCompareObjectId sourceObj) + { + try + { + if (sourceObj == null || sourceObj.NameParts == null || string.IsNullOrEmpty(sourceObj.SqlObjectType)) + { + return null; + } + ObjectIdentifier id = new ObjectIdentifier(sourceObj.NameParts); + SchemaComparisonExcludedObjectId excludedObjId = new SchemaComparisonExcludedObjectId(sourceObj.SqlObjectType, id); + return excludedObjId; + } + catch (ArgumentException) + { + return null; + } + } + + internal static SchemaCompareEndpoint CreateSchemaCompareEndpoint(SchemaCompareEndpointInfo endpointInfo, string connectionString) + { + switch (endpointInfo.EndpointType) + { + case SchemaCompareEndpointType.Dacpac: + { + return new SchemaCompareDacpacEndpoint(endpointInfo.PackageFilePath); + } + case SchemaCompareEndpointType.Database: + { + return new SchemaCompareDatabaseEndpoint(connectionString); + } + default: + { + throw new NotSupportedException($"Endpoint Type {endpointInfo.EndpointType} is not supported"); + } + } + } + + internal static string GetConnectionString(ConnectionInfo connInfo, string databaseName) + { + if (connInfo == null) + { + return null; + } + + connInfo.ConnectionDetails.DatabaseName = databaseName; + return ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); + } + + + internal static string RemoveExcessWhitespace(string script) + { + if (script != null) + { + // remove leading and trailing whitespace + script = script.Trim(); + // replace all multiple spaces with single space + script = Regex.Replace(script, " {2,}", " "); + } + return script; + } + + internal static string FormatScript(string script) + { + script = RemoveExcessWhitespace(script); + if (!string.IsNullOrWhiteSpace(script) && !script.Equals("null")) + { + script += Environment.NewLine + "GO"; + } + return script; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceOptionsTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceOptionsTests.cs index c17be9b4..5694543a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceOptionsTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceOptionsTests.cs @@ -17,7 +17,7 @@ using Xunit; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SchemaCompare { /// - /// Group of tests to test non-default options and included items for schema comapre + /// Group of tests to test non-default options and included items for schema compare /// Note - adding it to new class for easy findability /// public class SchemaCompareServiceOptionsTests @@ -106,8 +106,10 @@ END return options; } - private async void SendAndValidateSchemaCompareRequestDacpacToDacpacWithOptions(string sourceScript, string targetScript, DeploymentOptions nodiffOption, DeploymentOptions shouldDiffOption) + private async Task SendAndValidateSchemaCompareRequestDacpacToDacpacWithOptions(string sourceScript, string targetScript, DeploymentOptions nodiffOption, DeploymentOptions shouldDiffOption) { + var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + // create dacpacs from databases SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, sourceScript, "SchemaCompareSource"); SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, targetScript, "SchemaCompareTarget"); @@ -158,7 +160,7 @@ END } } - private async void SendAndValidateSchemaCompareRequestDatabaseToDatabaseWithOptions(string sourceScript, string targetScript, DeploymentOptions nodiffOption, DeploymentOptions shouldDiffOption) + private async Task SendAndValidateSchemaCompareRequestDatabaseToDatabaseWithOptions(string sourceScript, string targetScript, DeploymentOptions nodiffOption, DeploymentOptions shouldDiffOption) { var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, sourceScript, "SchemaCompareSource"); @@ -210,7 +212,7 @@ END } } - private async void SendAndValidateSchemaCompareGenerateScriptRequestDacpacToDatabaseWithOptions(string sourceScript, string targetScript, DeploymentOptions nodiffOption, DeploymentOptions shouldDiffOption) + private async Task SendAndValidateSchemaCompareGenerateScriptRequestDacpacToDatabaseWithOptions(string sourceScript, string targetScript, DeploymentOptions nodiffOption, DeploymentOptions shouldDiffOption) { var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, sourceScript, "SchemaCompareSource"); @@ -298,54 +300,54 @@ END /// Verify the schema compare request comparing two dacpacs with and without ignore column option /// [Fact] - public void SchemaCompareDacpacToDacpacOptions() + public async void SchemaCompareDacpacToDacpacOptions() { - SendAndValidateSchemaCompareRequestDacpacToDacpacWithOptions(Source1, Target1, GetIgnoreColumnOptions(), new DeploymentOptions()); + await SendAndValidateSchemaCompareRequestDacpacToDacpacWithOptions(Source1, Target1, GetIgnoreColumnOptions(), new DeploymentOptions()); } /// /// Verify the schema compare request comparing two dacpacs with and excluding table valued functions /// [Fact] - public void SchemaCompareDacpacToDacpacObjectTypes() + public async void SchemaCompareDacpacToDacpacObjectTypes() { - SendAndValidateSchemaCompareRequestDacpacToDacpacWithOptions(Source2, Target2, GetExcludeTableValuedFunctionOptions(), new DeploymentOptions()); + await SendAndValidateSchemaCompareRequestDacpacToDacpacWithOptions(Source2, Target2, GetExcludeTableValuedFunctionOptions(), new DeploymentOptions()); } /// /// Verify the schema compare request comparing two databases with and without ignore column option /// [Fact] - public void SchemaCompareDatabaseToDatabaseOptions() + public async void SchemaCompareDatabaseToDatabaseOptions() { - SendAndValidateSchemaCompareRequestDatabaseToDatabaseWithOptions(Source1, Target1, GetIgnoreColumnOptions(), new DeploymentOptions()); + await SendAndValidateSchemaCompareRequestDatabaseToDatabaseWithOptions(Source1, Target1, GetIgnoreColumnOptions(), new DeploymentOptions()); } /// /// Verify the schema compare request comparing two databases with and excluding table valued functions /// [Fact] - public void SchemaCompareDatabaseToDatabaseObjectTypes() + public async void SchemaCompareDatabaseToDatabaseObjectTypes() { - SendAndValidateSchemaCompareRequestDatabaseToDatabaseWithOptions(Source2, Target2, GetExcludeTableValuedFunctionOptions(), new DeploymentOptions()); + await SendAndValidateSchemaCompareRequestDatabaseToDatabaseWithOptions(Source2, Target2, GetExcludeTableValuedFunctionOptions(), new DeploymentOptions()); } /// /// Verify the schema compare script generation comparing dacpac and db with and without ignore column option /// [Fact] - public void SchemaCompareGenerateScriptDacpacToDatabaseOptions() + public async void SchemaCompareGenerateScriptDacpacToDatabaseOptions() { - SendAndValidateSchemaCompareGenerateScriptRequestDacpacToDatabaseWithOptions(Source1, Target1, GetIgnoreColumnOptions(), new DeploymentOptions()); + await SendAndValidateSchemaCompareGenerateScriptRequestDacpacToDatabaseWithOptions(Source1, Target1, GetIgnoreColumnOptions(), new DeploymentOptions()); } /// /// Verify the schema compare script generation comparing dacpac and db with and excluding table valued function /// [Fact] - public void SchemaCompareGenerateScriptDacpacToDatabaseObjectTypes() + public async void SchemaCompareGenerateScriptDacpacToDatabaseObjectTypes() { - SendAndValidateSchemaCompareGenerateScriptRequestDacpacToDatabaseWithOptions(Source2, Target2, GetExcludeTableValuedFunctionOptions(), new DeploymentOptions()); + await SendAndValidateSchemaCompareGenerateScriptRequestDacpacToDatabaseWithOptions(Source2, Target2, GetExcludeTableValuedFunctionOptions(), new DeploymentOptions()); } /// @@ -367,26 +369,7 @@ END dacOptions.IgnoreSemicolonBetweenStatements = false; dacOptions.IgnoreWhitespace = false; - System.Reflection.PropertyInfo[] deploymentOptionsProperties = deployOptions.GetType().GetProperties(); - System.Reflection.PropertyInfo[] ddProperties = dacOptions.GetType().GetProperties(); - - // Note that DatabaseSpecification and sql cmd variables list is not present in Sqltools service - its not settable and is not used by ADS options. - // TODO : update this test if the above options are added later - Assert.True(deploymentOptionsProperties.Length == ddProperties.Length - 2, $"Number of properties is not same Deployment options : {deploymentOptionsProperties.Length} DacFx options : {ddProperties.Length}"); - - foreach (var deployOptionsProp in deploymentOptionsProperties) - { - var dacProp = dacOptions.GetType().GetProperty(deployOptionsProp.Name); - Assert.True(dacProp != null, $"DacDeploy property not present for {deployOptionsProp.Name}"); - - var deployOptionsValue = deployOptionsProp.GetValue(deployOptions); - var dacValue = dacProp.GetValue(dacOptions); - - if (deployOptionsProp.Name != "ExcludeObjectTypes") // do not compare for ExcludeObjectTypes because it will be different - { - Assert.True((deployOptionsValue == null && dacValue == null) || deployOptionsValue.Equals(dacValue), $"DacFx DacDeploy property not equal to Tools Service DeploymentOptions for { deployOptionsProp.Name}, SchemaCompareOptions value: {deployOptionsValue} and DacDeployOptions value: {dacValue} "); - } - } + SchemaCompareTestUtils.CompareOptions(deployOptions, dacOptions); } /// @@ -398,32 +381,10 @@ END DeploymentOptions deployOptions = new DeploymentOptions(); var schemaCompareRequestContext = new Mock>(); schemaCompareRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); - schemaCompareRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((options) => this.OptionsEqualsDefault(options) == true))).Returns(Task.FromResult(new object())); - + schemaCompareRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((options) => SchemaCompareTestUtils.ValidateOptionsEqualsDefault(options) == true))).Returns(Task.FromResult(new object())); + SchemaCompareGetOptionsParams p = new SchemaCompareGetOptionsParams(); await SchemaCompareService.Instance.HandleSchemaCompareGetDefaultOptionsRequest(p, schemaCompareRequestContext.Object); } - - private bool OptionsEqualsDefault(SchemaCompareOptionsResult options) - { - DeploymentOptions defaultOpt = new DeploymentOptions(); - DeploymentOptions actualOpt = options.DefaultDeploymentOptions; - - System.Reflection.PropertyInfo[] deploymentOptionsProperties = defaultOpt.GetType().GetProperties(); - foreach (var v in deploymentOptionsProperties) - { - var defaultP = v.GetValue(defaultOpt); - var actualP = v.GetValue(actualOpt); - if (v.Name == "ExcludeObjectTypes") - { - Assert.True((defaultP as ObjectType[]).Length == (actualP as ObjectType[]).Length, $"Number of excluded objects is different; expected: {(defaultP as ObjectType[]).Length} actual: {(actualP as ObjectType[]).Length}"); - } - else - { - Assert.True((defaultP == null && actualP == null) || defaultP.Equals(actualP), $"Actual Property from Service is not equal to default property for { v.Name}, Actual value: {actualP} and Default value: {defaultP}"); - } - } - return true; - } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs index de70a874..d611fb66 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs @@ -3,13 +3,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using Microsoft.SqlServer.Dac.Compare; +using Microsoft.SqlServer.Dac.Model; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.SchemaCompare; using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; using Microsoft.SqlTools.ServiceLayer.TaskServices; using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.Utility; using Moq; using System; +using System.Data.SqlClient; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -48,6 +51,8 @@ CREATE TABLE [dbo].[table3] [Fact] public async void SchemaCompareDacpacToDacpac() { + var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + // create dacpacs from databases SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceScript, "SchemaCompareSource"); SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetScript, "SchemaCompareTarget"); @@ -171,6 +176,9 @@ CREATE TABLE [dbo].[table3] public async void SchemaCompareGenerateScriptDatabaseToDatabase() { var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + var schemaCompareRequestContext = new Mock>(); + schemaCompareRequestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceScript, "SchemaCompareSource"); SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetScript, "SchemaCompareTarget"); @@ -394,6 +402,230 @@ CREATE TABLE [dbo].[table3] } } + /// + /// Verify the schema compare Scmp File Save for database endpoints + /// + [Fact] + public async void SchemaCompareSaveScmpFileForDatabases() + { + var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceScript, "SchemaCompareSource"); + SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetScript, "SchemaCompareTarget"); + + try + { + SchemaCompareEndpointInfo sourceInfo = new SchemaCompareEndpointInfo(); + SchemaCompareEndpointInfo targetInfo = new SchemaCompareEndpointInfo(); + + sourceInfo.EndpointType = SchemaCompareEndpointType.Database; + sourceInfo.DatabaseName = sourceDb.DatabaseName; + targetInfo.EndpointType = SchemaCompareEndpointType.Database; + targetInfo.DatabaseName = targetDb.DatabaseName; + + CreateAndValidateScmpFile(sourceInfo, targetInfo, true, true); + } + finally + { + sourceDb.Cleanup(); + targetDb.Cleanup(); + } + } + + /// + /// Verify the schema compare Scmp File Save for dacpac endpoints + /// + [Fact] + public async void SchemaCompareSaveScmpFileForDacpacs() + { + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceScript, "SchemaCompareSource"); + SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetScript, "SchemaCompareTarget"); + + try + { + string sourceDacpac = SchemaCompareTestUtils.CreateDacpac(sourceDb); + string targetDacpac = SchemaCompareTestUtils.CreateDacpac(targetDb); + string filePath = SchemaCompareTestUtils.CreateScmpPath(); + + SchemaCompareEndpointInfo sourceInfo = new SchemaCompareEndpointInfo(); + SchemaCompareEndpointInfo targetInfo = new SchemaCompareEndpointInfo(); + + sourceInfo.EndpointType = SchemaCompareEndpointType.Dacpac; + sourceInfo.PackageFilePath = sourceDacpac; + targetInfo.EndpointType = SchemaCompareEndpointType.Dacpac; + targetInfo.PackageFilePath = targetDacpac; + + CreateAndValidateScmpFile(sourceInfo, targetInfo, false, false); + } + finally + { + sourceDb.Cleanup(); + targetDb.Cleanup(); + } + } + + /// + /// Verify the schema compare Scmp File Save for dacpac and db endpoints combination + /// + [Fact] + public async void SchemaCompareSaveScmpFileForDacpacToDB() + { + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceScript, "SchemaCompareSource"); + SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetScript, "SchemaCompareTarget"); + + try + { + string sourceDacpac = SchemaCompareTestUtils.CreateDacpac(sourceDb); + string filePath = SchemaCompareTestUtils.CreateScmpPath(); + + SchemaCompareEndpointInfo sourceInfo = new SchemaCompareEndpointInfo(); + SchemaCompareEndpointInfo targetInfo = new SchemaCompareEndpointInfo(); + + sourceInfo.EndpointType = SchemaCompareEndpointType.Dacpac; + sourceInfo.PackageFilePath = sourceDacpac; + targetInfo.EndpointType = SchemaCompareEndpointType.Database; + targetInfo.DatabaseName = targetDb.DatabaseName; + + CreateAndValidateScmpFile(sourceInfo, targetInfo, false, true); + } + finally + { + sourceDb.Cleanup(); + targetDb.Cleanup(); + } + } + + /// + /// Verify opening an scmp comparing two databases + /// + [Fact] + public async void SchemaCompareOpenScmpDatabaseToDatabaseRequest() + { + await CreateAndOpenScmp(SchemaCompareEndpointType.Database, SchemaCompareEndpointType.Database); + } + + /// + /// Verify opening an scmp comparing a dacpac and database + /// + [Fact] + public async void SchemaCompareOpenScmpDacpacToDatabaseRequest() + { + await CreateAndOpenScmp(SchemaCompareEndpointType.Dacpac, SchemaCompareEndpointType.Database); + } + + /// + /// Verify opening an scmp comparing two dacpacs + /// + [Fact] + public async void SchemaCompareOpenScmpDacpacToDacpacRequest() + { + await CreateAndOpenScmp(SchemaCompareEndpointType.Dacpac, SchemaCompareEndpointType.Dacpac); + } + + /// + /// Verify the schema compare Service Calls ends to end + /// + [Fact] + public async Task VerifySchemaCompareServiceCalls() + { + string operationId = null; + DiffEntry diffEntry = null; + var connectionObject = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceScript, "SchemaCompareSource"); + SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetScript, "SchemaCompareTarget"); + + try + { + SchemaCompareEndpointInfo sourceInfo = new SchemaCompareEndpointInfo(); + SchemaCompareEndpointInfo targetInfo = new SchemaCompareEndpointInfo(); + + sourceInfo.EndpointType = SchemaCompareEndpointType.Database; + sourceInfo.DatabaseName = sourceDb.DatabaseName; + sourceInfo.OwnerUri = connectionObject.ConnectionInfo.OwnerUri; + targetInfo.EndpointType = SchemaCompareEndpointType.Database; + targetInfo.DatabaseName = targetDb.DatabaseName; + targetInfo.OwnerUri = connectionObject.ConnectionInfo.OwnerUri; + + // Schema compare service call + var schemaCompareRequestContext = new Mock>(); + schemaCompareRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((diffResult) => + ValidateScResult(diffResult, ref diffEntry, ref operationId)))).Returns(Task.FromResult(new object())); + + var schemaCompareParams = new SchemaCompareParams + { + SourceEndpointInfo = sourceInfo, + TargetEndpointInfo = targetInfo, + DeploymentOptions = new DeploymentOptions() + }; + + await SchemaCompareService.Instance.HandleSchemaCompareRequest(schemaCompareParams, schemaCompareRequestContext.Object); + await SchemaCompareService.Instance.CurrentSchemaCompareTask; + + // Generate script Service call + var generateScriptRequestContext = new Mock>(); + generateScriptRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((result) => result.Success == true))).Returns(Task.FromResult(new object())); + + var generateScriptParams = new SchemaCompareGenerateScriptParams + { + OperationId = operationId, + TargetDatabaseName = targetDb.DatabaseName, + TargetServerName = "My server" + }; + + await SchemaCompareService.Instance.HandleSchemaCompareGenerateScriptRequest(generateScriptParams, generateScriptRequestContext.Object); + + // Publish service call + var publishRequestContext = new Mock>(); + publishRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((result) => result.Success == true))).Returns(Task.FromResult(new object())); + + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(targetDb.ConnectionString); + var publishParams = new SchemaCompareGenerateScriptParams + { + OperationId = operationId, + TargetDatabaseName = targetDb.DatabaseName, + TargetServerName = builder.DataSource, + }; + + await SchemaCompareService.Instance.HandleSchemaCompareGenerateScriptRequest(publishParams, publishRequestContext.Object); + + // Include/Exclude service call + var excludeRequestContext = new Mock>(); + excludeRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((result) => result.Success == true))).Returns(Task.FromResult(new object())); + + var excludeParams = new SchemaCompareNodeParams + { + OperationId = operationId, + DiffEntry = diffEntry + }; + + await SchemaCompareService.Instance.HandleSchemaCompareIncludeExcludeNodeRequest(excludeParams, publishRequestContext.Object); + + // Save Scmp service call + var saveScmpRequestContext = new Mock>(); + saveScmpRequestContext.Setup((RequestContext x) => x.SendResult(It.Is((result) => result.Success == true))).Returns(Task.FromResult(new object())); + var scmpFilePath = SchemaCompareTestUtils.CreateScmpPath(); + + var saveScmpParams = new SchemaCompareSaveScmpParams + { + SourceEndpointInfo = sourceInfo, + TargetEndpointInfo = targetInfo, + DeploymentOptions = new DeploymentOptions(), + ScmpFilePath = scmpFilePath + }; + + await SchemaCompareService.Instance.HandleSchemaCompareSaveScmpRequest(saveScmpParams, publishRequestContext.Object); + await SchemaCompareService.Instance.CurrentSchemaCompareTask; + SchemaCompareTestUtils.VerifyAndCleanup(scmpFilePath); + } + finally + { + sourceDb.Cleanup(); + targetDb.Cleanup(); + } + } + private void ValidateSchemaCompareWithExcludeIncludeResults(SchemaCompareOperation schemaCompareOperation) { schemaCompareOperation.Execute(TaskExecutionMode.Execute); @@ -403,7 +635,7 @@ CREATE TABLE [dbo].[table3] Assert.NotNull(schemaCompareOperation.ComparisonResult.Differences); // create Diff Entry from Difference - DiffEntry diff = SchemaCompareOperation.CreateDiffEntry(schemaCompareOperation.ComparisonResult.Differences.First(), null); + DiffEntry diff = SchemaCompareUtils.CreateDiffEntry(schemaCompareOperation.ComparisonResult.Differences.First(), null); int initial = schemaCompareOperation.ComparisonResult.Differences.Count(); SchemaCompareNodeParams schemaCompareExcludeNodeParams = new SchemaCompareNodeParams() @@ -451,7 +683,10 @@ CREATE TABLE [dbo].[table3] string initialScript = generateScriptOperation.ScriptGenerationResult.Script; // create Diff Entry from on Difference - DiffEntry diff = SchemaCompareOperation.CreateDiffEntry(schemaCompareOperation.ComparisonResult.Differences.First(), null); + DiffEntry diff = SchemaCompareUtils.CreateDiffEntry(schemaCompareOperation.ComparisonResult.Differences.First(), null); + + //Validate Diff Entry creation for object type + ValidateDiffEntryCreation(diff, schemaCompareOperation.ComparisonResult.Differences.First()); int initial = schemaCompareOperation.ComparisonResult.Differences.Count(); SchemaCompareNodeParams schemaCompareExcludeNodeParams = new SchemaCompareNodeParams() @@ -497,32 +732,6 @@ CREATE TABLE [dbo].[table3] Assert.True(initialScript.Length == afterIncludeScript.Length, $"Changes should be same as inital since we included what we excluded, before {initialScript}, now {afterIncludeScript}"); } - /// - /// Verify opening an scmp comparing two databases - /// - [Fact] - public async void SchemaCompareOpenScmpDatabaseToDatabaseRequest() - { - await CreateAndOpenScmp(SchemaCompareEndpointType.Database, SchemaCompareEndpointType.Database); - } - - /// - /// Verify opening an scmp comparing a dacpac and database - /// - [Fact] - public async void SchemaCompareOpenScmpDacpacToDatabaseRequest() - { - await CreateAndOpenScmp(SchemaCompareEndpointType.Dacpac, SchemaCompareEndpointType.Database); - } - - /// - /// Verify opening an scmp comparing two dacpacs - /// - [Fact] - public async void SchemaCompareOpenScmpDacpacToDacpacRequest() - { - await CreateAndOpenScmp(SchemaCompareEndpointType.Dacpac, SchemaCompareEndpointType.Dacpac); - } private async Task CreateAndOpenScmp(SchemaCompareEndpointType sourceEndpointType, SchemaCompareEndpointType targetEndpointType) { @@ -608,5 +817,122 @@ CREATE TABLE [dbo].[table3] Assert.Contains(resultEndpoint.ConnectionDetails.ConnectionString, connectionString); // connectionString has password but resultEndpoint doesn't } } + + private void ValidateDiffEntryCreation(DiffEntry diff, SchemaDifference schemaDifference) + { + if (schemaDifference.SourceObject != null) + { + ValidateDiffEntryObjects(diff.SourceValue, diff.SourceObjectType, schemaDifference.SourceObject); + } + if (schemaDifference.TargetObject != null) + { + ValidateDiffEntryObjects(diff.TargetValue, diff.TargetObjectType, schemaDifference.TargetObject); + } + } + + private void ValidateDiffEntryObjects(string[] diffObjectName, string diffObjectTypeType, TSqlObject dacfxObject) + { + Assert.Equal(dacfxObject.Name.Parts.Count, diffObjectName.Length); + for (int i = 0; i < diffObjectName.Length; i++) + { + Assert.Equal(dacfxObject.Name.Parts[i], diffObjectName[i]); + } + + var dacFxExcludedObject = new SchemaComparisonExcludedObjectId(dacfxObject.ObjectType, dacfxObject.Name); + var excludedObject = new SchemaComparisonExcludedObjectId(diffObjectTypeType, new ObjectIdentifier(diffObjectName)); + + Assert.Equal(dacFxExcludedObject.Identifier.ToString(), excludedObject.Identifier.ToString()); + Assert.Equal(dacFxExcludedObject.TypeName, excludedObject.TypeName); + + string dacFxType = dacFxExcludedObject.TypeName; + Assert.Equal(dacFxType, diffObjectTypeType); + } + + private void CreateAndValidateScmpFile(SchemaCompareEndpointInfo sourceInfo, SchemaCompareEndpointInfo targetInfo, bool isSourceDb, bool isTargetDb) + { + string filePath = SchemaCompareTestUtils.CreateScmpPath(); + var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + + SchemaCompareObjectId[] schemaCompareObjectIds = new SchemaCompareObjectId[]{ + new SchemaCompareObjectId() + { + NameParts = new string[] {"dbo", "Table1" }, + SqlObjectType = "Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable", + } + }; + + var schemaCompareParams = new SchemaCompareSaveScmpParams + { + SourceEndpointInfo = sourceInfo, + TargetEndpointInfo = targetInfo, + DeploymentOptions = new DeploymentOptions() + { + // change some random ones explicitly + AllowDropBlockingAssemblies = true, + DropConstraintsNotInSource = true, + IgnoreAnsiNulls = true, + NoAlterStatementsToChangeClrTypes = false, + PopulateFilesOnFileGroups = false, + VerifyDeployment = false, + }, + ScmpFilePath = filePath, + ExcludedSourceObjects = schemaCompareObjectIds, + ExcludedTargetObjects = null, + }; + + SchemaCompareSaveScmpOperation schemaCompareOperation = new SchemaCompareSaveScmpOperation(schemaCompareParams, result.ConnectionInfo, result.ConnectionInfo); + schemaCompareOperation.Execute(TaskExecutionMode.Execute); + + Assert.True(File.Exists(filePath), "SCMP file should be present"); + + string text = File.ReadAllText(filePath); + Assert.True(!string.IsNullOrEmpty(text), "SCMP File should not be empty"); + + // Validate with DacFx SchemaComparison object + SchemaComparison sc = new SchemaComparison(filePath); + + if (isSourceDb) + { + Assert.True(sc.Source is SchemaCompareDatabaseEndpoint, "Source should be SchemaCompareDatabaseEndpoint"); + Assert.True((sc.Source as SchemaCompareDatabaseEndpoint).DatabaseName == sourceInfo.DatabaseName, $"Source Database {(sc.Source as SchemaCompareDatabaseEndpoint).DatabaseName} name does not match the params passed {sourceInfo.DatabaseName}"); + } + else + { + Assert.True(sc.Source is SchemaCompareDacpacEndpoint, "Source should be SchemaCompareDacpacEndpoint"); + Assert.True((sc.Source as SchemaCompareDacpacEndpoint).FilePath == sourceInfo.PackageFilePath, $"Source dacpac {(sc.Source as SchemaCompareDacpacEndpoint).FilePath} name does not match the params passed {sourceInfo.PackageFilePath}"); + SchemaCompareTestUtils.VerifyAndCleanup(sourceInfo.PackageFilePath); + } + + if (isTargetDb) + { + Assert.True(sc.Target is SchemaCompareDatabaseEndpoint, "Source should be SchemaCompareDatabaseEndpoint"); + Assert.True((sc.Target as SchemaCompareDatabaseEndpoint).DatabaseName == targetInfo.DatabaseName, $"Source Database {(sc.Target as SchemaCompareDatabaseEndpoint).DatabaseName} name does not match the params passed {targetInfo.DatabaseName}"); + } + else + { + Assert.True(sc.Target is SchemaCompareDacpacEndpoint, "Source should be SchemaCompareDacpacEndpoint"); + Assert.True((sc.Target as SchemaCompareDacpacEndpoint).FilePath == targetInfo.PackageFilePath, $"Source dacpac {(sc.Target as SchemaCompareDacpacEndpoint).FilePath} name does not match the params passed {targetInfo.PackageFilePath}"); + SchemaCompareTestUtils.VerifyAndCleanup(targetInfo.PackageFilePath); + } + + Assert.True(!sc.ExcludedTargetObjects.Any(), "Target Excluded Objects are expected to be Empty"); + Assert.True(sc.ExcludedSourceObjects.Count == 1, $"Exactly {1} Source Excluded Object Should be present but {sc.ExcludedSourceObjects.Count} found"); + SchemaCompareTestUtils.CompareOptions(schemaCompareParams.DeploymentOptions, sc.Options); + SchemaCompareTestUtils.VerifyAndCleanup(filePath); + } + + private bool ValidateScResult(SchemaCompareResult diffResult, ref DiffEntry diffEntry, ref string operationId) + { + try + { + operationId = diffResult.OperationId; + diffEntry = diffResult.Differences.ElementAt(0); + return (diffResult.Success == true && diffResult.Differences != null && diffResult.Differences.Count > 0); + } + catch + { + return false; + } + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareTestUtils.cs index 9393f311..2cc59e94 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareTestUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareTestUtils.cs @@ -3,10 +3,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.SqlServer.Dac; using Microsoft.SqlTools.ServiceLayer.DacFx; using Microsoft.SqlTools.ServiceLayer.DacFx.Contracts; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using NUnit.Framework; using System; @@ -50,6 +52,18 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SchemaCompare return extractParams.PackageFilePath; } + internal static string CreateScmpPath() + { + var result = GetLiveAutoCompleteTestObjects(); + string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SchemaCompareTest"); + Directory.CreateDirectory(folderPath); + string fileName = TestContext.CurrentContext?.Test?.Name + "_" + DateTime.Now.Ticks.ToString(); + + string path = Path.Combine(folderPath, string.Format("{0}.scmp", fileName)); + + return path; + } + internal static LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects() { // Adding retry for reliability - otherwise it caused test to fail in lab @@ -69,5 +83,52 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SchemaCompare return result; } + + internal static void CompareOptions(DeploymentOptions deploymentOptions, DacDeployOptions dacDeployOptions) + { + System.Reflection.PropertyInfo[] deploymentOptionsProperties = deploymentOptions.GetType().GetProperties(); + System.Reflection.PropertyInfo[] dacDeployProperties = dacDeployOptions.GetType().GetProperties(); + + // Note that DatabaseSpecification and sql cmd variables list is not present in Sqltools service - its not settable and is not used by ADS options. + // They are not present in SSDT as well + // TODO : update this test if the above options are added later + Assert.True(deploymentOptionsProperties.Length == dacDeployProperties.Length - 2, $"Number of properties is not same Deployment options : {deploymentOptionsProperties.Length} DacFx options : {dacDeployProperties.Length}"); + + foreach (var deployOptionsProp in deploymentOptionsProperties) + { + var dacProp = dacDeployOptions.GetType().GetProperty(deployOptionsProp.Name); + Assert.True(dacProp != null, $"DacDeploy property not present for {deployOptionsProp.Name}"); + + var deployOptionsValue = deployOptionsProp.GetValue(deploymentOptions); + var dacValue = dacProp.GetValue(dacDeployOptions); + + if (deployOptionsProp.Name != "ExcludeObjectTypes") // do not compare for ExcludeObjectTypes because it will be different + { + Assert.True((deployOptionsValue == null && dacValue == null) || deployOptionsValue.Equals(dacValue), $"DacFx DacDeploy property not equal to Tools Service DeploymentOptions for { deployOptionsProp.Name}, SchemaCompareOptions value: {deployOptionsValue} and DacDeployOptions value: {dacValue} "); + } + } + } + + internal static bool ValidateOptionsEqualsDefault(SchemaCompareOptionsResult options) + { + DeploymentOptions defaultOpt = new DeploymentOptions(); + DeploymentOptions actualOpt = options.DefaultDeploymentOptions; + + System.Reflection.PropertyInfo[] deploymentOptionsProperties = defaultOpt.GetType().GetProperties(); + foreach (var v in deploymentOptionsProperties) + { + var defaultP = v.GetValue(defaultOpt); + var actualP = v.GetValue(actualOpt); + if (v.Name == "ExcludeObjectTypes") + { + Assert.True((defaultP as ObjectType[]).Length == (actualP as ObjectType[]).Length, $"Number of excluded objects is different; expected: {(defaultP as ObjectType[]).Length} actual: {(actualP as ObjectType[]).Length}"); + } + else + { + Assert.True((defaultP == null && actualP == null) || defaultP.Equals(actualP), $"Actual Property from Service is not equal to default property for { v.Name}, Actual value: {actualP} and Default value: {defaultP}"); + } + } + return true; + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SchemaCompare/SchemaCompareTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SchemaCompare/SchemaCompareTests.cs index 8a315c2a..e6908bf4 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SchemaCompare/SchemaCompareTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SchemaCompare/SchemaCompareTests.cs @@ -5,6 +5,7 @@ using System; using Microsoft.SqlTools.ServiceLayer.SchemaCompare; +using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare @@ -16,7 +17,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare { string script = "EXECUTE sp_addextendedproperty @name = N'MS_Description', @value = N'Primary key for AWBuildVersion records.', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'AWBuildVersion', @level2type = N'COLUMN', @level2name = N'SystemInformationID';"; Assert.DoesNotContain("GO", script); - string result = SchemaCompareOperation.FormatScript(script); + string result = SchemaCompareUtils.FormatScript(script); Assert.EndsWith("GO", result); } @@ -24,12 +25,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare public void FormatScriptDoesNotAddGoForNullScripts() { string script1 = null; - string result1 = SchemaCompareOperation.FormatScript(script1); + string result1 = SchemaCompareUtils.FormatScript(script1); Assert.DoesNotContain("GO", result1); Assert.Equal(null, result1); string script2 = "null"; - string result2 = SchemaCompareOperation.FormatScript(script2); + string result2 = SchemaCompareUtils.FormatScript(script2); Assert.DoesNotContain("GO", result2); } @@ -37,7 +38,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare public void FormatScriptDoesNotAddGoForEmptyStringScripts() { string script = string.Empty; - string result = SchemaCompareOperation.FormatScript(script); + string result = SchemaCompareUtils.FormatScript(script); Assert.DoesNotContain("GO", result); Assert.Equal(string.Empty, result); } @@ -47,7 +48,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare { string script = " \t\n"; Assert.True(string.IsNullOrWhiteSpace(script)); - string result = SchemaCompareOperation.FormatScript(script); + string result = SchemaCompareUtils.FormatScript(script); Assert.DoesNotContain("GO", result); Assert.Equal(string.Empty, result); } @@ -57,14 +58,14 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare { // leading whitespace string script1 = "\r\n EXECUTE sp_addextendedproperty @name = N'MS_Description', @value = N'Primary key for AWBuildVersion records.', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'AWBuildVersion', @level2type = N'COLUMN', @level2name = N'SystemInformationID';"; - string result1 = SchemaCompareOperation.RemoveExcessWhitespace(script1); + string result1 = SchemaCompareUtils.RemoveExcessWhitespace(script1); Assert.False(script1.Equals(result1)); Assert.False(result1.StartsWith("\r")); Assert.True(result1.StartsWith("EXECUTE")); // trailing whitespace string script2 = "EXECUTE sp_addextendedproperty @name = N'MS_Description', @value = N'Primary key for AWBuildVersion records.', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'AWBuildVersion', @level2type = N'COLUMN', @level2name = N'SystemInformationID'; \n"; - string result2 = SchemaCompareOperation.RemoveExcessWhitespace(script2); + string result2 = SchemaCompareUtils.RemoveExcessWhitespace(script2); Assert.False(script2.Equals(result2)); Assert.False(result2.EndsWith("\n")); Assert.True(result2.EndsWith(";")); @@ -82,8 +83,54 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SchemaCompare [VersionDate] DATETIME NOT NULL, [ModifiedDate] DATETIME NOT NULL );"; - string result3 = SchemaCompareOperation.RemoveExcessWhitespace(script3); + string result3 = SchemaCompareUtils.RemoveExcessWhitespace(script3); Assert.True(expected3.Equals(result3)); } + + [Fact] + public void CreateExcludedObjects() + { + //successful creation + ValidateTableCreation(new string[] { "dbo", "Table1" }, "dbo.Table1"); + ValidateTableCreation(new string[] { "[dbo]", "Table.1" }, "[dbo].Table.1"); + + //null creation due to null name + SchemaCompareObjectId object1 = new SchemaCompareObjectId + { + NameParts = null, //null caused by this value + SqlObjectType = "Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable" + }; + + var nullResult1 = SchemaCompareUtils.CreateExcludedObject(object1); + Assert.Null(nullResult1); + + //null creation due to argumentException + SchemaCompareObjectId object2 = new SchemaCompareObjectId + { + NameParts = new string[] { "dbo", "Table1" }, + SqlObjectType = "SqlTable" // null caused by this value + }; + + var nullResult2 = SchemaCompareUtils.CreateExcludedObject(object2); + Assert.Null(nullResult2); + } + + private void ValidateTableCreation(string[] nameParts, string validationString) + { + SchemaCompareObjectId validObject1 = new SchemaCompareObjectId + { + NameParts = nameParts, + SqlObjectType = "Microsoft.Data.Tools.Schema.Sql.SchemaModel.SqlTable" + }; + var validResult1 = SchemaCompareUtils.CreateExcludedObject(validObject1); + Assert.NotNull(validResult1); + Assert.Equal(validObject1.SqlObjectType, validResult1.TypeName); + Assert.Equal(validObject1.NameParts.Length, validResult1.Identifier.Parts.Count); + Assert.Equal(validationString, string.Join(".", validResult1.Identifier.Parts)); + for (int i = 0; i < validObject1.NameParts.Length; i++) + { + Assert.Equal(validObject1.NameParts[i], validResult1.Identifier.Parts[i]); + } + } } }