11

I'm trying to use CF to build a model for an existing database. I have a column in which I forgot to set a sane default value. And rather than compromise the purity of the initial migration by changing it, I just figured I'd create another migration (that's what migrations are for, right? :)

public override void Up()
{
    AlterColumn("Config", "DefaultTaxPerDollar", c => c.Decimal(nullable: false, precision: 19, scale: 5, defaultValue: 0.087m));
}

public override void Down()
{
    AlterColumn("Config", "DefaultTaxPerDollar", c => c.Decimal(nullable: false, precision: 19, scale: 5, defaultValue: 0.0m));

}

But this produces Column already has a DEFAULT bound to it. error from the SQL Server.

How does one change a default value using CF migrations? Or, how does one simply remove a default value (and subsequently re-create it with a different value)?

Edit:

Here is the SQL generated:

ALTER TABLE [Config] ADD CONSTRAINT DF_DefaultTaxPerDollar DEFAULT 0.087 FOR [DefaultTaxPerDollar]
ALTER TABLE [Config] ALTER COLUMN [DefaultTaxPerDollar] [decimal](19, 5) NOT NULL

I think I may have found a solution, to use the Sql() method with some complex SQL inspired by this post. The problem stems from the fact that SQL Server uses constraints to implement defaults (OH! how I miss MySQL!) with a generated name for the constraint. So the Code First team could not simply change or remove/re-create the default value easily.

HiredMind
  • 1,827
  • 17
  • 27
  • Can you post what SQL is generated? – Ladislav Mrnka Aug 15 '12 at 18:01
  • 1
    Yes that was my point. I expected that migrations only add constraint but do not check / delete existing one. I think this could be considered as a bug. – Ladislav Mrnka Aug 15 '12 at 21:31
  • Can you provide your solution as an answer? – Ladislav Mrnka Aug 15 '12 at 21:32
  • Will do - as soon as I nail it down :) I'm pretty sure this will work, but due to the fact that the actual SQL is built out of AlterColumnOperation objects and then sent to the server all at once, my custom SQL has to play nice with all the generated SQL, and it's proving to be a bit tricky. Will post when I get it working. – HiredMind Aug 15 '12 at 23:17
  • 2
    FYI, this should be fixed in E6 (See [Work Item 452](https://entityframework.codeplex.com/workitem/452)) – bricelam Jun 16 '13 at 02:35
  • @Brice: That's strange, I filed that work item and didn't get notified that the status had changed. Oh well, still good news! Thanks for the FYI. – HiredMind Jun 20 '13 at 20:11
  • I found a similar issue in EF 6.1 where the max length of a column can be changed after is it created (work item [2247](https://entityframework.codeplex.com/workitem/2247) – Kevin Kuszyk Apr 30 '14 at 10:00
  • Yeah, since posting this question I've found quite a few similar issues. All of them seem to be related to SQL Server's rather bizarre idiosyncrasies. Of course I'm biased - having learned the much simpler and much more elegant MySQL before SQL Server. One of the reasons I liked the idea of EF is so I wouldn't have to deal with all of SQL Server's language strangeness but I can't get away from it, haha! I don't envy the EF team. – HiredMind Apr 30 '14 at 16:07

2 Answers2

15

Removal of default constraints inspired by reverse migrations produced by Entity Framework for SQL Server

    public static void DropDefaultConstraint(string tableName, string columnName, Action<string> executeSQL)
    {
        string constraintVariableName = string.Format("@constraint_{0}", Guid.NewGuid().ToString("N"));

        string sql = string.Format(@"
            DECLARE {0} nvarchar(128)
            SELECT {0} = name
            FROM sys.default_constraints
            WHERE parent_object_id = object_id(N'{1}')
            AND col_name(parent_object_id, parent_column_id) = '{2}';
            IF {0} IS NOT NULL
                EXECUTE('ALTER TABLE {1} DROP CONSTRAINT ' + {0})",
            constraintVariableName,
            tableName,
            columnName);

        executeSQL(sql);
    }

It's slightly shorter, but the usage is the same.

DropDefaultConstraint(TableName, "DefaultTaxPerDollar", q => Sql(q));

The Guid is used to make a unique variable name in case you are going to drop several constraints in one migration.

Mike
  • 2,468
  • 3
  • 25
  • 36
  • +1, but the square brackets in `ALTER TABLE [{1}] ...` should be removed. They are causing an error with schema qualified table names when calling `DropDefaultConstraint("Sales.Orders", ...)` for example. – Slauma Jun 13 '13 at 18:09
  • 1
    if my contraintname contain character '.', will cause error. So I changed this line " EXECUTE('ALTER TABLE {1} DROP CONSTRAINT ' + {0})", " into " EXECUTE('ALTER TABLE {1} DROP CONSTRAINT [' + {0} + ']')", " , it works. – heavenwing Oct 01 '14 at 06:02
  • @heavenwing just wanted to say thanks for your note, it helped me fix this issue in my code. – NotMyself Mar 12 '15 at 17:09
6

Here's a solution that was inspired by this post. It's not exactly an elegant method, but it works for me.


        public static void DropDefaultConstraint(string tableName, string columnName, Action executeSQL)
        {
            // Execute query that drops the UDF that finds the default constraint name
            var query = @"
                    -- recreate UDF 
                    if object_id('[dbo].[GetDefaultConstraintName]') is not null
                    begin 
                        drop function [dbo].[GetDefaultConstraintName]
                    end
                ";
            executeSQL(query);

            // Execute query that (re)creates UDF that finds the default constraint name
            query = @"
                    create function [dbo].[GetDefaultConstraintName] (
                        @TableName varchar(max),
                        @ColumnName varchar(max))
                    returns varchar(max)
                    as
                    begin
                        -- Returns the name of the default constraint for a column

                        declare @Command varchar(max)
                        select
                            @Command = d.name
                        from
                            ((
                            sys.tables t join
                            sys.default_constraints d
                                on
                                    d.parent_object_id = t.object_id) join
                            sys.columns c
                                on
                                    c.object_id = t.object_id and
                                    c.column_id = d.parent_column_id)
                        where
                            t.name = @TableName and
                            c.name = @ColumnName
                        return @Command
                    end
                ";
            executeSQL(query);

            // Execute query that actually drops the constraint
            query = string.Format(@"
                    -- Use UDF to find constraint name
                    DECLARE @Constraint_Name VARCHAR(100)
                    SET @Constraint_Name = [dbo].GetDefaultConstraintName('{0}','{1}')

                    if LEN(@Constraint_Name) > 0 
                    BEGIN
                        DECLARE @query VARCHAR(300)
                        SET @query = 'ALTER TABLE {0} DROP CONSTRAINT ' + @Constraint_Name

                        execute(@query)
                    END", tableName, columnName);
            executeSQL(query);
        }

And in your migration, you can call it like this:

DropDefaultConstraint(TableName, "DefaultTaxPerDollar", q => Sql(q));

The reason for using the lamba is because you have to make three distinct calls to Sql(). I was never able to get this to work as one long query - tried many combinations of the keyword GO in many different places. I also tried reversing the logic on the first query so that the UDF only gets recreated if it does not exist, and it didn't work. I suppose recreating it every time is more robust anyway.

HiredMind
  • 1,827
  • 17
  • 27