Skip to content

Instantly share code, notes, and snippets.

@guillaume86
Created May 15, 2026 13:11
Show Gist options
  • Select an option

  • Save guillaume86/d5a4ce3c6ba1b21f2fd35433ce560561 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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);
}
}
<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