Created
May 15, 2026 13:11
-
-
Save guillaume86/d5a4ce3c6ba1b21f2fd35433ce560561 to your computer and use it in GitHub Desktop.
Minimal DacFx repro: PublishChangesToProject emits a duplicate ALTER TABLE ADD CONSTRAINT for a constraint already defined inline in CREATE TABLE. Reproduces on both Microsoft.Build.Sql SDK and legacy SSDT projects with DacFx 170.4.80-preview.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Minimal self-contained repro for DacFx issue: | |
| // SchemaComparisonResult.PublishChangesToProject emits a redundant | |
| // trailing ALTER TABLE ADD CONSTRAINT for a constraint already defined | |
| // inline within CREATE TABLE. | |
| // | |
| // Usage: | |
| // dotnet run -- [server] [sa-password] | |
| // Defaults: server=localhost,11433 password=MpleoDevTools_Pwd1! | |
| using Microsoft.Data.SqlClient; | |
| using Microsoft.SqlServer.Dac; | |
| using Microsoft.SqlServer.Dac.Compare; | |
| class Program | |
| { | |
| const string LegacySqlproj = """ | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0"> | |
| <PropertyGroup> | |
| <Name>Repro</Name> | |
| <SchemaVersion>2.0</SchemaVersion> | |
| <ProjectVersion>4.1</ProjectVersion> | |
| <ProjectGuid>{11111111-1111-1111-1111-111111111111}</ProjectGuid> | |
| <DSP>Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider</DSP> | |
| <OutputType>Database</OutputType> | |
| <DefaultCollation>SQL_Latin1_General_CP1_CI_AS</DefaultCollation> | |
| <ModelCollation>1033, CI</ModelCollation> | |
| </PropertyGroup> | |
| <ItemGroup> | |
| <Build Include="dbo\Tables\Demo.sql" /> | |
| </ItemGroup> | |
| <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /> | |
| </Project> | |
| """; | |
| const string SdkSqlproj = """ | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <Project DefaultTargets="Build"> | |
| <Sdk Name="Microsoft.Build.Sql" Version="2.1.0" /> | |
| <PropertyGroup> | |
| <Name>Repro</Name> | |
| <ProjectGuid>{22222222-2222-2222-2222-222222222222}</ProjectGuid> | |
| <DSP>Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider</DSP> | |
| <ModelCollation>1033, CI</ModelCollation> | |
| <DefaultCollation>SQL_Latin1_General_CP1_CI_AS</DefaultCollation> | |
| </PropertyGroup> | |
| </Project> | |
| """; | |
| static int Main(string[] args) | |
| { | |
| var server = args.Length > 0 ? args[0] : "localhost,11433"; | |
| var saPwd = args.Length > 1 ? args[1] : "MpleoDevTools_Pwd1!"; | |
| var workDir = Path.Combine(Path.GetTempPath(), "DacFxRepro_" + Guid.NewGuid().ToString("N")[..8]); | |
| Directory.CreateDirectory(workDir); | |
| Console.WriteLine($"Work dir: {workDir}"); | |
| var dbName = "DacFxReproDb_" + Guid.NewGuid().ToString("N")[..8]; | |
| var masterConn = $"Server={server};User Id=sa;Password={saPwd};Encrypt=False;TrustServerCertificate=True"; | |
| var dbConn = $"Server={server};User Id=sa;Password={saPwd};Database={dbName};Encrypt=False;TrustServerCertificate=True"; | |
| Exec(masterConn, $"CREATE DATABASE [{dbName}];"); | |
| try | |
| { | |
| // Live DB starts with the same shape the project will start with: | |
| // a 2-column table, no inline DEFAULT. | |
| Exec(dbConn, """ | |
| CREATE TABLE [dbo].[Demo] ( | |
| [Id] INT NOT NULL PRIMARY KEY, | |
| [Flag] BIT NOT NULL | |
| ); | |
| """); | |
| RunScenario("LEGACY", LegacySqlproj, workDir, dbConn); | |
| // Reset DB for the next scenario. | |
| Exec(dbConn, "DROP TABLE [dbo].[Demo]; CREATE TABLE [dbo].[Demo] ([Id] INT NOT NULL PRIMARY KEY, [Flag] BIT NOT NULL);"); | |
| RunScenario("SDK", SdkSqlproj, workDir, dbConn); | |
| } | |
| finally | |
| { | |
| try { Exec(masterConn, $"ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{dbName}];"); } catch { } | |
| } | |
| return 0; | |
| } | |
| static void RunScenario(string label, string sqlprojXml, string baseDir, string dbConn) | |
| { | |
| Console.WriteLine(); | |
| Console.WriteLine($"=========== {label} ==========="); | |
| var dir = Path.Combine(baseDir, label); | |
| Directory.CreateDirectory(Path.Combine(dir, "dbo", "Tables")); | |
| var sqlprojPath = Path.Combine(dir, "Repro.sqlproj"); | |
| File.WriteAllText(sqlprojPath, sqlprojXml); | |
| // Project starts with 2 columns and no DEFAULT — matches DB state. | |
| var tablePath = Path.Combine(dir, "dbo", "Tables", "Demo.sql"); | |
| File.WriteAllText(tablePath, """ | |
| CREATE TABLE [dbo].[Demo] ( | |
| [Id] INT NOT NULL PRIMARY KEY, | |
| [Flag] BIT NOT NULL | |
| ); | |
| """); | |
| Console.WriteLine($"[{label}] Target project: {sqlprojPath}"); | |
| Console.WriteLine($"[{label}] Initial Demo.sql:"); | |
| PrintIndented(File.ReadAllText(tablePath)); | |
| var dsp = "Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider"; | |
| var scripts = new[] { tablePath }; | |
| // Mutate the DB so it has an additional column + inline DEFAULT | |
| // that the project doesn't have. DacFx should add both to the | |
| // project — but it should add the DEFAULT only ONCE. | |
| Exec(dbConn, """ | |
| ALTER TABLE [dbo].[Demo] ADD [Note] NVARCHAR(50) NULL CONSTRAINT [DF_Demo_Note] DEFAULT ('hello'); | |
| """); | |
| var source = new SchemaCompareDatabaseEndpoint(dbConn); | |
| var target = new SchemaCompareProjectEndpoint(sqlprojPath, scripts, dsp, DacExtractTarget.SchemaObjectType); | |
| var cmp = new SchemaComparison(source, target); | |
| var result = cmp.Compare(); | |
| Console.WriteLine($"[{label}] Compare: {result.Differences?.Count() ?? 0} difference(s)."); | |
| var publish = result.PublishChangesToProject(dir, DacExtractTarget.SchemaObjectType); | |
| Console.WriteLine($"[{label}] PublishChangesToProject.Success = {publish.Success}"); | |
| if (!publish.Success) | |
| { | |
| Console.WriteLine($"[{label}] Error: {publish.ErrorMessage}"); | |
| return; | |
| } | |
| Console.WriteLine($"[{label}] Added: {string.Join(", ", publish.AddedFiles)}"); | |
| Console.WriteLine($"[{label}] Changed: {string.Join(", ", publish.ChangedFiles)}"); | |
| Console.WriteLine($"[{label}] Deleted: {string.Join(", ", publish.DeletedFiles)}"); | |
| var after = File.ReadAllText(tablePath); | |
| Console.WriteLine($"[{label}] Demo.sql AFTER publish:"); | |
| PrintIndented(after); | |
| var inlineHit = after.Contains("CONSTRAINT [DF_Demo_Note] DEFAULT"); | |
| var standaloneHit = after.Contains("ADD CONSTRAINT [DF_Demo_Note]"); | |
| Console.WriteLine($"[{label}] Inline CONSTRAINT [DF_Demo_Note] present: {inlineHit}"); | |
| Console.WriteLine($"[{label}] Standalone ADD CONSTRAINT [DF_Demo_Note]: {standaloneHit}"); | |
| Console.WriteLine($"[{label}] >>> BUG: " + (inlineHit && standaloneHit | |
| ? "REPRODUCED — duplicate constraint emitted." | |
| : "not reproduced in this run.")); | |
| } | |
| static void Exec(string connStr, string sql) | |
| { | |
| using var conn = new SqlConnection(connStr); | |
| conn.Open(); | |
| using var cmd = new SqlCommand(sql, conn) { CommandTimeout = 60 }; | |
| cmd.ExecuteNonQuery(); | |
| } | |
| static void PrintIndented(string text) | |
| { | |
| foreach (var line in text.Replace("\r", "").Split('\n')) | |
| Console.WriteLine(" " + line); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <Project Sdk="Microsoft.NET.Sdk"> | |
| <PropertyGroup> | |
| <OutputType>Exe</OutputType> | |
| <TargetFramework>net10.0</TargetFramework> | |
| <Nullable>enable</Nullable> | |
| <ImplicitUsings>enable</ImplicitUsings> | |
| <RollForward>LatestMajor</RollForward> | |
| </PropertyGroup> | |
| <ItemGroup> | |
| <PackageReference Include="Microsoft.SqlServer.DacFx" Version="170.4.80-preview" /> | |
| <PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" /> | |
| </ItemGroup> | |
| </Project> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment