diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index 2db9ac95..45b6f51c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareIncludeExcludeNodeRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareIncludeExcludeNodeRequest.cs index ae9e8071..aa9e2c3f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareIncludeExcludeNodeRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareIncludeExcludeNodeRequest.cs @@ -6,6 +6,7 @@ using Microsoft.SqlTools.Hosting.Protocol.Contracts; using Microsoft.SqlTools.ServiceLayer.TaskServices; using Microsoft.SqlTools.ServiceLayer.Utility; +using System.Collections.Generic; namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts { @@ -40,4 +41,12 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts public static readonly RequestType Type = RequestType.Create("schemaCompare/includeExcludeNode"); } + + /// + /// Parameters returned from a schema compare include/exclude request. + /// + public class SchemaCompareIncludeExcludeResult : ResultStatus + { + public List ChangedDifferences { get; set; } + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs index 0f1621ed..0db7e4f6 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/Contracts/SchemaCompareRequest.cs @@ -101,6 +101,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts public string TargetScript { get; set; } public string SourceObjectType { get; set; } public string TargetObjectType { get; set; } + public bool Included { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs index 1e244c8e..537000ef 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareIncludeExcludeNodeOperation.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using System.Linq; namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare { @@ -38,6 +39,8 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare public bool Success { get; set; } + public List ChangedDifferences; + public SchemaCompareIncludeExcludeNodeOperation(SchemaCompareNodeParams parameters, SchemaComparisonResult comparisonResult) { Validate.IsNotNull("parameters", parameters); @@ -46,6 +49,11 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare this.ComparisonResult = comparisonResult; } + /// + /// Exclude will return false if included dependencies are found. Include will also include dependencies that need to be included. + /// This is the same behavior as SSDT + /// + /// public void Execute(TaskExecutionMode mode) { this.CancellationToken.ThrowIfCancellationRequested(); @@ -58,7 +66,27 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare throw new InvalidOperationException(SR.SchemaCompareExcludeIncludeNodeNotFound); } + // Check first if the dependencies will allow this if it's an exclude request + if (!this.Parameters.IncludeRequest) + { + IEnumerable dependencies = this.ComparisonResult.GetExcludeDependencies(node); + + bool block = dependencies.Any(d => d.Included); + if (block) + { + this.Success = false; + return; + } + } + this.Success = this.Parameters.IncludeRequest ? this.ComparisonResult.Include(node) : this.ComparisonResult.Exclude(node); + + // create list of affected dependencies of this request + if (this.Success) + { + IEnumerable dependencies = this.ComparisonResult.GetIncludeDependencies(node); + this.ChangedDifferences = dependencies.Select(difference => SchemaCompareUtils.CreateDiffEntry(difference, null)).ToList(); + } } catch (Exception e) { @@ -97,10 +125,16 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare System.Reflection.PropertyInfo[] properties = diffEntry.GetType().GetProperties(); foreach (var prop in properties) { - result = result && - ((prop.GetValue(diffEntry) == null && - prop.GetValue(entryFromDifference) == null) || - prop.GetValue(diffEntry).SafeToString().Equals(prop.GetValue(entryFromDifference).SafeToString())); + // Don't need to check if included is the same when verifying if the difference is equal + if (prop.Name != "Included") + { + if(!((prop.GetValue(diffEntry) == null && + prop.GetValue(entryFromDifference) == null) || + prop.GetValue(diffEntry).SafeToString().Equals(prop.GetValue(entryFromDifference).SafeToString()))) + { + return false; + } + } } return result; diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs index 94f1687d..7df42d91 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareService.cs @@ -245,10 +245,17 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare operation.Execute(parameters.TaskExecutionMode); - await requestContext.SendResult(new ResultStatus() + // update the comparison result if the include/exclude was successful + if(operation.Success) { - Success = true, - ErrorMessage = operation.ErrorMessage + schemaCompareResults.Value[parameters.OperationId] = operation.ComparisonResult; + } + + await requestContext.SendResult(new SchemaCompareIncludeExcludeResult() + { + Success = operation.Success, + ErrorMessage = operation.ErrorMessage, + ChangedDifferences = operation.ChangedDifferences }); } catch (Exception e) diff --git a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs index b39b8c1b..e1eb7fcd 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/SchemaCompare/SchemaCompareUtils.cs @@ -48,6 +48,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SchemaCompare diffEntry.UpdateAction = difference.UpdateAction; diffEntry.DifferenceType = difference.DifferenceType; diffEntry.Name = difference.Name; + diffEntry.Included = difference.Included; if (difference.SourceObject != null) { diff --git a/test/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests.csproj b/test/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests.csproj index e702bb54..5ea6d8fd 100644 --- a/test/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests.csproj +++ b/test/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests/Microsoft.SqlTools.ManagedBatchParser.IntegrationTests.csproj @@ -32,7 +32,7 @@ - + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj index 6d417602..de0b71e3 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj @@ -33,7 +33,7 @@ - + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs index 7d9b857c..fa323634 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SchemaCompare/SchemaCompareServiceTests.cs @@ -45,6 +45,19 @@ CREATE TABLE [dbo].[table3] [col1] INT NULL, )"; + private const string SourceIncludeExcludeScript = @"CREATE TABLE t1(c1 INT PRIMARY KEY, c2 INT) +GO +CREATE TABLE t2(c1 INT PRIMARY KEY, c2 INT) +GO +cREATE VIEW v1 as SELECT c1 FROM t2"; + + private const string TargetIncludeExcludeScript = @"CREATE TABLE t1 (c1 INT PRIMARY KEY) +GO +CREATE TABLE t3 (c3 INT PRIMARY KEY, c2 INT) +GO +CREATE VIEW v2 as SELECT t1.c1, t3.c3 FROM t1, t3 +GO"; + private const string CreateKey = @"CREATE COLUMN MASTER KEY [CMK_Auto1] WITH ( KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', @@ -801,7 +814,102 @@ WITH VALUES sourceDb.Cleanup(); targetDb.Cleanup(); } + } + /// + /// Verify the schema compare request with failing exclude request because of dependencies and that include will include dependencies + /// + [Fact] + public async void SchemaCompareIncludeExcludeWithDependencies() + { + var result = SchemaCompareTestUtils.GetLiveAutoCompleteTestObjects(); + SqlTestDb sourceDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, SourceIncludeExcludeScript, "SchemaCompareSource"); + SqlTestDb targetDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, TargetIncludeExcludeScript, "SchemaCompareTarget"); + + try + { + string targetDacpacFilePath = SchemaCompareTestUtils.CreateDacpac(targetDb); + + SchemaCompareEndpointInfo sourceInfo = new SchemaCompareEndpointInfo(); + SchemaCompareEndpointInfo targetInfo = new SchemaCompareEndpointInfo(); + + sourceInfo.EndpointType = SchemaCompareEndpointType.Database; + sourceInfo.DatabaseName = sourceDb.DatabaseName; + targetInfo.EndpointType = SchemaCompareEndpointType.Dacpac; + targetInfo.PackageFilePath = targetDacpacFilePath; + + var schemaCompareParams = new SchemaCompareParams + { + SourceEndpointInfo = sourceInfo, + TargetEndpointInfo = targetInfo + }; + + SchemaCompareOperation schemaCompareOperation = new SchemaCompareOperation(schemaCompareParams, result.ConnectionInfo, null); + schemaCompareOperation.Execute(TaskExecutionMode.Execute); + Assert.True(schemaCompareOperation.ComparisonResult.IsValid); + Assert.False(schemaCompareOperation.ComparisonResult.IsEqual); + Assert.NotNull(schemaCompareOperation.ComparisonResult.Differences); + + // try to exclude + DiffEntry t2Diff = SchemaCompareUtils.CreateDiffEntry(schemaCompareOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "t2").First(), null); + SchemaCompareNodeParams t2ExcludeParams = new SchemaCompareNodeParams() + { + OperationId = schemaCompareOperation.OperationId, + DiffEntry = t2Diff, + IncludeRequest = false, + TaskExecutionMode = TaskExecutionMode.Execute + }; + + SchemaCompareIncludeExcludeNodeOperation t2ExcludeOperation = new SchemaCompareIncludeExcludeNodeOperation(t2ExcludeParams, schemaCompareOperation.ComparisonResult); + t2ExcludeOperation.Execute(TaskExecutionMode.Execute); + Assert.False(t2ExcludeOperation.Success, "Excluding Table t2 should fail because view v1 depends on it"); + Assert.True(t2ExcludeOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "t2").First().Included, "Difference Table t2 should still be included because the exclude request failed"); + + // exclude view first, then excluding t2 should work + DiffEntry v1Diff = SchemaCompareUtils.CreateDiffEntry(schemaCompareOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "v1").First(), null); + SchemaCompareNodeParams v1ExcludeParams = new SchemaCompareNodeParams() + { + OperationId = schemaCompareOperation.OperationId, + DiffEntry = v1Diff, + IncludeRequest = false, + TaskExecutionMode = TaskExecutionMode.Execute + }; + + SchemaCompareIncludeExcludeNodeOperation v1ExcludeOperation = new SchemaCompareIncludeExcludeNodeOperation(v1ExcludeParams, schemaCompareOperation.ComparisonResult); + v1ExcludeOperation.Execute(TaskExecutionMode.Execute); + Assert.True(v1ExcludeOperation.Success, "Excluding View v1 should succeed"); + Assert.False(t2ExcludeOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "v1").First().Included, "Difference View v1 should be excluded"); + + // try to exclude t2 again and it should succeed this time + t2ExcludeOperation.Execute(TaskExecutionMode.Execute); + Assert.True(t2ExcludeOperation.Success, "Excluding Table t2 should succeed"); + Assert.False(t2ExcludeOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "t2").First().Included, "Difference Table t2 should still be excluded"); + + // including v1 should also include t2 + SchemaCompareNodeParams v1IncludeParams = new SchemaCompareNodeParams() + { + OperationId = schemaCompareOperation.OperationId, + DiffEntry = v1Diff, + IncludeRequest = true, + TaskExecutionMode = TaskExecutionMode.Execute + }; + + SchemaCompareIncludeExcludeNodeOperation v1IncludeOperation = new SchemaCompareIncludeExcludeNodeOperation(v1IncludeParams, t2ExcludeOperation.ComparisonResult); + v1IncludeOperation.Execute(TaskExecutionMode.Execute); + Assert.True(v1IncludeOperation.Success, "Including v1 should succeed"); + Assert.True(v1IncludeOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "v1").First().Included, "Difference View v1 should be included"); + Assert.True(v1IncludeOperation.ComparisonResult.Differences.Where(x => x.SourceObject != null && x.SourceObject.Name.Parts[1] == "t2").First().Included, "Difference Table t2 should still be included"); + Assert.True(v1IncludeOperation.ChangedDifferences != null && v1IncludeOperation.ChangedDifferences.Count == 1, "There should be one difference"); + Assert.True(v1IncludeOperation.ChangedDifferences.First().SourceValue[1] == "t2", "The affected difference of including v1 should be t2"); + + // cleanup + SchemaCompareTestUtils.VerifyAndCleanup(targetDacpacFilePath); + } + finally + { + sourceDb.Cleanup(); + targetDb.Cleanup(); + } } private void ValidateSchemaCompareWithExcludeIncludeResults(SchemaCompareOperation schemaCompareOperation)