Inconsistent default constraints from SQL Server Management Objects (SMO)
Asked Answered
G

3

12

I have program that generates DDL scripts for a Microsoft SQL Server database using SQL Server Management Objects (SMO). However, depending on the server and database, I receive inconsistent output of default constraints for tables. Sometimes they are inline with the CREATE TABLE statement, and sometimes they are standalone ALTER TABLE statements. I realize that both are valid and correct SQL-statements, but without consistency it prevents automated comparison between the output of multiple databases and prevents adding the output to source control to track changes of the database schema. How can I ensure consistency in the script output of default constraints?

Sample Program

The code should be straight forward. Opens the server and database, then generates individual script files for each database object plus one more file that contains a script for the entire database. I've omitted a lot of error checking and database objects that appear to generate consistent output already.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;
using System.Data.SqlClient;
using System.IO;
using System.Configuration;
using System.Runtime.Serialization;
using System.Data;

namespace Stackoverflow.Sample
{
    class Program
    {
        public static void CreateScripts(SqlConnectionStringBuilder source, string destination)
        {
            Server sv = new Server(source.DataSource);
            sv.ConnectionContext.LoginSecure = false;
            sv.ConnectionContext.Login = source.UserID;
            sv.ConnectionContext.Password = source.Password;
            sv.ConnectionContext.ConnectionString = source.ConnectionString;

            Database db = sv.Databases[source.InitialCatalog];

            ScriptingOptions options = new ScriptingOptions();
            options.ScriptData = false;
            options.ScriptDrops = false;
            options.ScriptSchema = true;
            options.EnforceScriptingOptions = true;
            options.Indexes = true;
            options.IncludeHeaders = true;
            options.ClusteredIndexes = true;
            options.WithDependencies = false;
            options.IncludeHeaders = false;
            options.DriAll = true;

            StringBuilder sbAll = new StringBuilder();

            Dictionary<string, TriggerCollection> tableTriggers = new Dictionary<string, TriggerCollection>();
            Dictionary<string, TriggerCollection> viewTriggers = new Dictionary<string, TriggerCollection>();

            // Code omitted for Functions

            // Tables
            foreach (Table table in db.Tables)
            {
                StringBuilder sbTable = new StringBuilder();
                foreach (string line in db.Tables[table.Name].Script(options))
                {
                    sbAll.Append(line + "\r\n");
                    sbTable.Append(line + "\r\n");
                    Console.WriteLine(line);
                }
                // Write file with DDL of individual object
                File.WriteAllText(Path.Combine(destination, table.Name + ".sql"), sbTable.ToString());

                if (table.Triggers.Count > 0)
                    tableTriggers.Add(table.Name, table.Triggers);
            }

            // Code omitted for Views, Stored Procedures, Table Triggers, View Triggers, Database Triggers, etc

            // Write file with full DDL of everything above
            string[] statements = sbAll.ToString().Split(new string[] { "\r\nGO\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            File.WriteAllLines(Path.Combine(destination, "Full.sql"), statements);
        }
    }
}

Sample Output of Inline Statements

A sample of what the output looks like when SMO generates scripts with inline statements for default constraints.

SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
CREATE TABLE [dbo].[Products](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NULL,
    [Name_En] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Name_Fr] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Type] [int] NOT NULL CONSTRAINT [DF_Products_Type]  DEFAULT ((0)),
    [ManagedType] [int] NOT NULL CONSTRAINT [DF_Products_ManagedType]  DEFAULT ((0)),
    [ProductFamilyID] [bigint] NOT NULL,
    [ImplementationID] [bigint] NOT NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_Implementations] FOREIGN KEY([ImplementationID])
REFERENCES [dbo].[Implementations] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_Implementations]
ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_ProductFamilies] FOREIGN KEY([ProductFamilyID])
REFERENCES [dbo].[ProductFamilies] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_ProductFamilies]

Sample Output of Standalone Statements

A sample of what the output looks like when SMO generates scripts with standalone statements for default constraints.

SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
CREATE TABLE [dbo].[Products](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NULL,
    [Name_En] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Name_Fr] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Type] [int] NOT NULL,
    [ManagedType] [int] NOT NULL,
    [ProductFamilyID] [bigint] NOT NULL,
    [ImplementationID] [bigint] NOT NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Products] ADD  CONSTRAINT [DF_Products_Type]  DEFAULT ((0)) FOR [Type]
ALTER TABLE [dbo].[Products] ADD  CONSTRAINT [DF_Products_ManagedType]  DEFAULT ((0)) FOR [ManagedType]
ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_Implementations] FOREIGN KEY([ImplementationID])
REFERENCES [dbo].[Implementations] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_Implementations]
ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_ProductFamilies] FOREIGN KEY([ProductFamilyID])
REFERENCES [dbo].[ProductFamilies] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_ProductFamilies]

Never appears to be a mixture within a single database, but can get different output styles per database on a single server. Haven't noticed it change over time for a database but perhaps I just haven't attempted generating scripts for a database over a long enough period of time. I've backed up and restored a database to another server and to the same server under a different name and it seems to randomly decide to choose one output style. Therefore, it doesn't seem like it could be a database setting when individual database restores can exhibit random behaviour.

Currently all servers used in testing have SQL Server 2012 installed and always running the code on the same workstation with SQL Server Management Studio 2012 installed. I've looked through the properties of ScriptingOptions on MSDN and I don't see anything that stands out as a solution.

Gavan answered 21/10, 2014 at 5:55 Comment(4)
The inline form can only be used if the column is being added at the same time. If the default is applied later, it has to be done by the standalone form. Perhaps what you're seeing just reflects the reality of how each database ended up in its current state.Jaw
I'm not calling shenanigans, @Damien_The_Unbeliever, but how does that difference bear out in the metadata (i.e. sys.default_constraints)? That is, how would the tool know the difference and act accordingly?Panicle
@Jaw Interesting thought. I have the same question back to you as Ben, where would this difference in current state be stored? I checked [DF_Products_Type] & [DF_Products_ManagedType] in sys.default_constraints and the only differences between the databases that generated the sample output in my question are the object_id, parent_object_id, create_date, and modify_date. I also checked [Products] in sys.tables and again the only difference was object_id, create_date, and modify_date. Within each row, create_date equals modify_date, and no create_date matched between any rows.Gavan
This issue seems to also affect Sql Server Management Studio (ssms) when using "tasks > generate scripts". I'm guessing ssms uses smo under the hood, but couldn't find confirmation of that online.Cassatt
G
3

After further investigation, I have discovered this is an issue with SQL Server Management Objects (SMO) and its handling of default constraints in versions 2012 and above. Others have reported related problems, such as the following Microsoft Connect issue: https://connect.microsoft.com/SQLServer/Feedback/Details/895113

While this answers why the default constraints from SQL Server Management Objects (SMO) are inconsistent, it isn't a solution. It's possible someone could determine a workaround to ensure consistency of the output before Microsoft gets around to fixing the issue. Thus this question is still open to other answers if you can find a workaround.

Gavan answered 23/10, 2014 at 0:0 Comment(2)
Title of the connect issue for the record "SMO 2014 scripts tables with defaults differently depending on rowcount = 0 or not"Cassatt
My testing confirms that adding a row to an otherwise empty table causes the default to be specified in the table definition instead of in a separate alter statement. Annoying.Cassatt
C
3

This workaround modifies the scripts that are generated by removing the individual ALTER TABLE ... ADD CONSTRAINT ... DEFAULT scripts and putting the definitions in the CREATE TABLE script. It gets the "works on my machine" badge.

Table table = GetTable();

List<string> scripts = table.Script(new ScriptingOptions
{
    DriAll = true,
    FullTextCatalogs = true,
    FullTextIndexes = true,
    Indexes = true,
    SchemaQualify = true
}).Cast<string>().ToList();

// There is a bug in the SQL SMO libraries that changes the scripting of the
// default constraints depending on whether or not the table has any rows.
// This hack gets around the issue by modifying the scripts to always include
// the constaints in the CREATE TABLE definition. 
// https://connect.microsoft.com/SQLServer/Feedback/Details/895113
//
// First, get the CREATE TABLE script to modify.
string originalCreateTableScript = scripts.Single(s => s.StartsWith("CREATE TABLE"));
string modifiedCreateTableScript = originalCreateTableScript;
bool modificationsMade = false;

// This pattern will match all ALTER TABLE scripts that define a default constraint.
Regex defineDefaultConstraintPattern = new Regex(@"^ALTER TABLE .+ ADD\s+CONSTRAINT \[(?<constraint_name>[^\]]+)]  DEFAULT (?<constraint_def>.+) FOR \[(?<column>.+)]$");

// Find all the matching scripts.
foreach (string script in scripts)
{
    Match defaultConstraintMatch = defineDefaultConstraintPattern.Match(script);

    if (defaultConstraintMatch.Success)
    {
        // We have found a default constraint script. The following pattern
        // will match the line in the CREATE TABLE script that defines the
        // column on which the constraint is defined.
        Regex columnPattern = new Regex(@"^(?<def1>\s*\[" + Regex.Escape(defaultConstraintMatch.Groups["column"].Value) + @"].+?)(?<def2>,?\r)$", RegexOptions.Multiline);

        // Replace the column definition with a definition that includes the constraint.
        modifiedCreateTableScript = columnPattern.Replace(modifiedCreateTableScript, delegate (Match columnMatch)
        {
            modificationsMade = true;
            return string.Format(
                "{0} CONSTRAINT [{1}]  DEFAULT {2}{3}",
                columnMatch.Groups["def1"].Value,
                defaultConstraintMatch.Groups["constraint_name"].Value,
                defaultConstraintMatch.Groups["constraint_def"].Value,
                columnMatch.Groups["def2"].Value);
        });
    }
}

if (modificationsMade)
{
    int ix = scripts.IndexOf(originalCreateTableScript);
    scripts[ix] = modifiedCreateTableScript;
    scripts.RemoveAll(s => defineDefaultConstraintPattern.IsMatch(s));
}
Coda answered 7/7, 2016 at 16:54 Comment(0)
M
1

I guess I have found the workaround. The only thing we have to do is to set internal field forceEmbedDefaultConstraint of DefaultConstraint class to true. To do so we have to use some reflection. Please execute code below on every table you wish to script and the default constraint definition will be added to column creation statement independently on rows count.

    private void ForceScriptDefaultConstraint(Table table)
    {
        foreach (Column column in table.Columns)
        {
            if (column.DefaultConstraint != null)
            {
                FieldInfo info = column.DefaultConstraint.GetType().GetField("forceEmbedDefaultConstraint", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
                info.SetValue(column.DefaultConstraint, true);
            }
        }
    }

For people asking for explanation why do I think it should work: Using dotPeek I have found a method in Microsoft.SqlServer.SMO.Column class:

private void ScriptDefaultConstraint(StringBuilder sb, ScriptingPreferences sp)
{
  if (this.DefaultConstraint == null || this.DefaultConstraint.IgnoreForScripting && !sp.ForDirectExecution || (!this.EmbedDefaultConstraints() && !this.DefaultConstraint.forceEmbedDefaultConstraint || sb.Length <= 0))
    return;
  this.DefaultConstraint.forceEmbedDefaultConstraint = false;
  sb.Append(this.DefaultConstraint.ScriptDdl(sp));
}

Code above convinced me to change the value of forceEmbedDefaultConstraint to true. In my case it worked, but the order of database objects creation may influence the final result.

Muliebrity answered 16/3, 2016 at 16:55 Comment(4)
Conceptually this looks promising; however, after testing the code it failed to make any difference on the tables that generate the ALTER TABLE style of default constraints. And since this attempts to manipulate private fields for which I do not have the source code, I have no options to debug and find out where it is failing, nor can I be sure it does anything at all (ex: for all I know the underlying bug in SMO might be that this field is ignored). Can you provide additional material on why you believe this should work?Gavan
Hi Sybeus. I have added some explanation to my previous post to explain how did I solved this problem in my scenario.Muliebrity
I can confirm, unfortunately, that this workaround does not work.Coda
Another confirmation that the internals change from version to version. Microsoft.SqlServer.Smo v11's version of Microsoft.SqlServer.Management.Smo.Column has its private variable named "scriptDefaultConstraintEmbedded", and the method body for ScriptDefaultConstratin() is significantly different.Sakovich

© 2022 - 2024 — McMap. All rights reserved.