206

I created a date column in a previous migration and set it to be nullable. Now I want to change it to be not nullable. How do I go about doing this assuming there are null rows in that database? I'm ok with setting those columns to Time.now if they're currently null.

Kevin Pang
  • 39,694
  • 37
  • 117
  • 169

8 Answers8

217

If you do it in a migration then you could probably do it like this:

# Make sure no null value exist
MyModel.where(date_column: nil).update_all(date_column: Time.now)

# Change the column to not allow null
change_column :my_models, :date_column, :datetime, null: false
Dorian
  • 19,009
  • 8
  • 108
  • 111
DanneManne
  • 20,521
  • 5
  • 53
  • 54
  • 1
    Just a note, because this made me bust my dev database. Rather use explicit hash syntax, like this: `MyModel.update_all({:date_column => Time.now}, {:date_column => nil})`. The query in your original form just made all my models have nil value in the field. – dimitarvp Aug 01 '12 at 12:16
  • Thanks for the update. I know this was not the case when I wrote this answer but I can't remember which version of Ruby or RoR I was using at the time. – DanneManne Aug 02 '12 at 01:36
  • 1
    Do you have the use the 'up'/'down' method in this migration, or can you the simple change method in the migration? – E.E.33 Aug 30 '12 at 02:34
  • 2
    The `change` method is not so suited for this case because (1) the `update_all` method will executed on both the migrate and a potential revert. That might not be the worst thing but because (2) the migration has no way of knowing what the column was changed from in a potential revert. So for this case I would stick with `up` and `down`. – DanneManne Aug 30 '12 at 04:03
  • can I add `MyModel.update_all({:date_column => Time.now}, {:date_column => nil})` to a migration file? – Kush Nov 04 '13 at 21:58
  • RE @DanneManne - in this case, on a down, the `date_column` will all have `NOT NULL` values, so the `update_all` statement will do nothing. So a `change` block is fine. – toobulkeh Nov 15 '13 at 05:10
  • RE @toobulkeh: True, but personally I would still use the double block. If not for the update_all, then for the change_column statement. Because the migration does not know what the state of that column was before it was changed. So what is it gonna revert it to? – DanneManne Nov 22 '13 at 08:17
  • You should never use the modal itself to make data changes in migrations. For example, you might remove the model (MyModel in this case) in a later migration. If someone would then run through all your migrations it would break. Rather execute SQL statements directly using ActiveRecord::Base.connection.execute('your sql here') – Jaco Pretorius May 10 '14 at 20:12
  • MyModel.where({:date_column => nil}).update_all({:date_column => Time.now}) - Rails 4 – gavit Nov 06 '14 at 16:41
  • 2
    For anyone interested, [my answer](http://stackoverflow.com/a/25211732/616644) shows how to do this in a single step. – Rick Smith Apr 06 '15 at 23:16
182

In Rails 4, this is a better (DRYer) solution:

change_column_null :my_models, :date_column, false

To ensure no records exist with NULL values in that column, you can pass a fourth parameter, which is the default value to use for records with NULL values:

change_column_null :my_models, :date_column, false, Time.now
Joshua Pinter
  • 37,288
  • 19
  • 208
  • 218
mrbrdo
  • 7,173
  • 4
  • 29
  • 36
  • 4
    This causes problems when the table already has null values. [See my answer](http://stackoverflow.com/a/25211732/616644) – Rick Smith Aug 08 '14 at 20:27
  • 5
    Also available in 3.2. Has a 4th parameter too for setting the default where value are null. – toxaq Oct 10 '14 at 02:13
  • 1
    Plus 1 for `change_column_null`. However Rick Smith's comment above points out a very valid case. – 0112 Nov 06 '14 at 17:05
  • Updated to add the query for updating null values. The 4th parameter (default value) is only useful when you actually want to have a default for future records as well. – mrbrdo Nov 14 '14 at 06:23
  • 5
    Actually, according to the Rails 4.2 docs, the 4th param does NOT set a default value for future records: "The method accepts an optional fourth argument to replace existing +NULL+s with some other value. Please note the fourth argument does not set a column’s default." – Mike Fischer Aug 23 '15 at 16:08
  • Just like @MikeFischer wrote, `change_column_null` does not set a default value for the column. If you need to do this, I would suggest using `update_all` to update all `null` records first and then using `change_column :table, :column, :column_type, default: "yourmom", null: false` instead. It's clearer and may even be faster/less blocking. – Joshua Pinter May 11 '21 at 15:23
70

Rails 4 (other Rails 4 answers have problems):

def change
  change_column_null(:users, :admin, false, <put a default value here> )
  # change_column(:users, :admin, :string, :default => "")
end

Changing a column with NULL values in it to not allow NULL will cause problems. This is exactly the type of code that will work fine in your development setup and then crash when you try to deploy it to your LIVE production. You should first change NULL values to something valid and then disallow NULLs. The 4th value in change_column_null does exactly that. See documentation for more details.

Also, I generally prefer to set a default value for the field so I won't need to specify the field's value every time I create a new object. I included the commented out code to do that as well.

Rick Smith
  • 8,219
  • 13
  • 75
  • 81
  • 3
    For Rails 4, this appears to be the most accurate and complete answer, including the commented-out default setting. – Mike Fischer Aug 23 '15 at 16:31
  • 4
    If you are adding a new column to a table and want to insert new values for null, but don't want to add a default value for the column, you can do this in your migration: `add_column :users, :admin, :string` then `change_column_null(:admin, :string, false, "new_value_for_existing_records")` – colsen Mar 05 '16 at 23:55
38

Create a migration that has a change_column statement with a :default => value.

change_column :my_table, :my_column, :integer, :default => 0, :null => false

See: change_column

Depending on the database engine you may need to use change_column_null

Soviut
  • 79,529
  • 41
  • 166
  • 227
jessecurry
  • 21,857
  • 8
  • 49
  • 44
  • 1
    This worked for me. Using MySql locally. When pushed and ran app in Heroku (Postgres) it crapped on column that was not null when I was writing it a null - rightfully so. Only "change_column_null" would work could not use "change_column ... :null => false" on MySql. Thanks. – rtfminc Jul 03 '11 at 19:27
  • 1
    so what was your migration after change_column_null – js111 Jun 05 '12 at 21:57
  • 1
    Postges is more strict that MySQL -- I'd expect that it would require `change_column_null`. – jessecurry Jun 05 '12 at 22:20
  • 3
    @rtfminc I strongly recommend you to use the same database engine in development and in production, as it avoids a lot of problems when it comes to edge cases. – yagooar Mar 23 '13 at 10:13
13

Rails 4:

def change
  change_column_null(:users, :admin, false )
end
James Chevalier
  • 9,724
  • 4
  • 42
  • 68
piratebroadcast
  • 293
  • 5
  • 15
3

In Rails 4.02+ according to the docs there is no method like update_all with 2 arguments. Instead one can use this code:

# Make sure no null value exist
MyModel.where(date_column: nil).update_all(date_column: Time.now)

# Change the column to not allow null
change_column :my_models, :date_column, :datetime, null: false
jmarceli
  • 15,970
  • 5
  • 57
  • 57
2

You can't use add_timestamps and null:false if you have existing records, so here is the solution :

def change
  add_timestamps(:buttons, null: true)

  Button.find_each { |b| b.update(created_at: Time.zone.now, updated_at: Time.zone.now) }

  change_column_null(:buttons, :created_at, false)
  change_column_null(:buttons, :updated_at, false)
end
Dorian
  • 19,009
  • 8
  • 108
  • 111
Nicolas Maloeuvre
  • 2,523
  • 19
  • 38
0

Per the Strong Migrations gem, using change_column_null in production is a bad idea because it blocks reads and writes while all records are checked.

The recommended way to handle these migrations (Postgres specific) is to separate this process into two migrations.

One to alter the table with the constraint:

class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
  def change
    safety_assured do
      execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
    end
  end
end

And a separate migration to validate it:

class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
  def change
    safety_assured do
      execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
    end
  end
end

The above examples are pulled (and slightly altered) from the linked documentation. Apparently for Postgres 12+ you can also add NOT NULL to the schema and then drop the constraint after the validation has been run:

class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
  def change
    safety_assured do
      execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
    end

    # in Postgres 12+, you can then safely set NOT NULL on the column
    change_column_null :users, :some_column, false
    safety_assured do
      execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
    end
  end
end

Naturally, this means your schema will not show that the column is NOT NULL for earlier versions of Postgres, so I'd also advise setting a model level validation to require the value to be present (though I'd suggest the same even for versions of PG that do allow this step).

Further, before running these migrations you'll want to update all existing records with a value other than null, and make sure any production code that writes to the table is not writing null for the value(s).

MattD
  • 3,837
  • 2
  • 31
  • 41