I have the problem, that I have an migration in Rails that sets up a default setting for a column, like this example:
def self.up
add_column :column_name, :bought_at, :datetime, :default => Time.now
end
Suppose, I like to drop that default settings in a later migration, how do I do that with using rails migrations?
My current workaround is the execution of a custom sql command in the rails migration, like this:
def self.up
execute 'alter table column_name alter bought_at drop default'
end
But I don't like this approach, because I am now dependent on how the underlying database is interpreting this command. In case of a change of the database this query perhaps might not work anymore and the migration would be broken. So, is there a way to express the undo of a default setting for a column in rails?
Rails 5+
def change
change_column_default( :table_name, :column_name, from: nil, to: false )
end
Rails 3 and Rails 4
def up
change_column_default( :table_name, :column_name, nil )
end
def down
change_column_default( :table_name, :column_name, false )
end
Sounds like you're doing the right thing with your 'execute', as the docs point out:
change_column_default(table_name, column_name, default)
Sets a new default value for a column.
If you want to set the default value
to NULL, you are out of luck. You need
to DatabaseStatements#execute the
appropriate SQL statement yourself.
Examples
change_column_default(:suppliers, :qualification, 'new')
change_column_default(:accounts, :authorized, 1)
The following snippet I use to make NULL columns NOT NULL, but skip DEFAULT at schema level:
def self.up
change_column :table, :column, :string, :null => false, :default => ""
change_column_default(:table, :column, nil)
end
Rails 4
change_column :courses, :name, :string, limit: 100, null: false
Related
The question is about rails database migration.
The current database contains two entries for a supposedly boolean variable as in the database scheme as follows:
create_table "table_name", force: :cascade do |t|
...
t.string "yes_boolvar"
t.string "no_boolvar"
...
end
I need to convert it to one single boolean variable as following:
t.boolean "boolvar"
I considered about renaming the 'yes_boolvar', changing its type from string to boolean, and then removing 'no_boolvar' column, based on some readings, like the following:
t.rename :yes_boolvar,
:boolvar
t.change :boolvar,
:boolean
t.remove :no_boolvar
However, this will only consider the truth value of 'yes_*' and not 'no_*' while copying the value of the variable. Is there a way to successfully migrate the var so that the truth (or nil) values of the both the vars are taken into account.
It depends on your app.
If no one can update those values (i.e. it is not a field in a user profile), then you can:
make a database dump
run your migration
execute a code that will fill boolvar
Another solution is to migrate data in 3 steps:
first migration renames column
second migration migrates data
third migration removes no_boolvar column
I guess it is possible to merge the first two actions into a single migration (but I prefer to keep them separated).
I recommends you to handle with 3 migrations.
For first, create a migration adding a boolean: :boolvar
class AddBoolvarToTableName < ActiveRecord::Migration
def up
add_column :table, :boolvar, :boolean
end
def down
remove_column :table, :boolvar
end
end
After, create a new migration to handle data:
class RepopulateBooleanValues < ActiveRecord::Migration[5.0]
def change
YourClass.all.each do |record|
# put the logic here like:
record.boolvar = record.yes_boolvar == 'true'
# or
record.boolvar = record.not_boolvar == 'false'
# I'am not sure whats the content of yes_boolvar and not_boolvar, elaborate the logic here
record.save
end
end
end
To finish it, just create a new migration removing yes_boolvar and no_boolvar.
This is roughly the migration I'd write (I didn't run the code, but it should work):
# This ensures the migration to work
# regardless the customizations on your original model
class TempModel < ActiveRecord::Base
self.table_name = 'table_name'
end
class MyMigration < ActiveRecord::Migration[5.0]
def up
add_column :table_name, :boolvar, :boolean
TempModel.reset_column_information
TempModel.find_each do |record|
# Decide some logic here about how to migrate values from yes_boolvar
# and no_boolvar columns to boolvar column
boolvar_value = record.yes_boolvar || !record.no_boolvar
record.update_column :boolvar, boolvar_value
end
remove_column :table_name, :yes_boolvar
remove_column :table_name, :no_boolvar
end
def down
add_column :table_name, :yes_boolvar, :string
add_column :table_name, :no_boolvar, :string
TempModel.reset_column_information
TempModel.find_each do |record|
# Decide some logic here about how to handle yes_boolvar
# and no_boolvar values
record.update_columns yes_boolvar: record.boolvar,
no_boolvar: !record.boolvar
end
remove_column :table_name, :boolvar
end
end
Trying to add Timestamps to existing table.
According to Api documenation add_timestamps
Here is my code in migration:
def change
add_timestamps(:products, null: false)
end
Getting error:
*-- add_timestamps(:products, {:null=>false})
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
SQLite3::SQLException: Cannot add a NOT NULL column with default value NULL: ALTER TABLE "products" ADD "created_at" datetime NOT NULL*
I've also tried all solution in this thread
Same error...
Rails 5.1.4
Ruby 2.4.0
You cannot add columns with not-null constraint to a non-empty table because the existing lines in the table would have empty values right away and therefore the condition fails.
Instead, introduce the columns in three steps:
def change
# add new column but allow null values
add_timestamps :products, null: true
# backfill existing records with created_at and updated_at
# values that make clear that the records are faked
long_ago = DateTime.new(2000, 1, 1)
Product.update_all(created_at: long_ago, updated_at: long_ago)
# change to not null constraints
change_column_null :products, :created_at, false
change_column_null :products, :updated_at, false
end
In my opinion, it is wrong to manipulate existing data with activerecord queries or even SQL in migrations.
The correct rails 5.2+ way to do this is :
class AddTimestampsToCars < ActiveRecord::Migration[5.2]
def change
add_timestamps :cars, null: false, default: -> { 'NOW()' }
end
end
It's a proc so you should be able to set a date in the past if you want to.
Source: https://github.com/rails/rails/pull/20005
I like #spickermann's approach since it takes into account the existing records and probably your migration already went all the way to production, his method ensures data perseverance.
Nevertheless, many of you guys might find yourselves in that situation, but still in development, meaning that there's no real sensitive data you might be afraid of losing... That gives you a bit more freedom on how you can perform the change in the table.
If your code and records only exist locally (if you still have no records created, just skip step 1.) and that table was created in the last migration , my suggestion is:
1.- Delete all the records from that table.
2.- Go to your migration file and edit it by adding t.timestamps so that it looks something like this:
class CreateInstitutionalLegals < ActiveRecord::Migration[5.0]
def change
create_table :institutional_legals do |t|
# Your original migration content goes here
.
.
t.timestamps # This is your addition
end
end
end
3.- Then go to your console and enter rails:db:redo. As explained here, that command is a shortcut for doing a rollback and then migrating back up again.
Now you will see that your schema is updated with the corresponding created_atand updated_at columns.
The concrete benefit of this is that it is super easy to do, you don't create an extra migration file and you learn to use a very handy command ;)
I'm on rails 5.0 and none of these options worked. The rails:db:redo will work but isn't a feasible solution for most.
The only thing that worked was
def change
add_column :products, :created_at, :timestamp
add_column :products, :updated_at, :timestamp
end
I had the same issue. I wanted the end result to be strictly equivalent to add_timestamps :products on an fresh database.
Instead of running a query to backfill, I ended up doing a 3-steps process.
add column with null allowed and default to current time to backfill
change constraint to not null
remove default
And it is reversible.
add_column :products, :created_at, :datetime, precision: 6, null: true, default: -> { "CURRENT_TIMESTAMP" }
add_column :products, :updated_at, :datetime, precision: 6, null: true, default: -> { "CURRENT_TIMESTAMP" }
change_column_null :products, :created_at, false
change_column_null :products, :updated_at, false
change_column_default :products, :created_at, from: -> { "CURRENT_TIMESTAMP" }, to: nil
change_column_default :products, :updated_at, from: -> { "CURRENT_TIMESTAMP" }, to: nil
NB: This is with Rails 6.1 and PostgreSQL
Is change_column method able to rollback when it is used in migrations in Rails 4 and above?
Here is an example:
def change
change_column etc
end
Should I use up and down methods instead?
change_column cannot be reverted by default.
Rails cannot know the column definition before the migration, so it cannot roll back to this original definition.
Therefor starting with Rails 4, you can provide Rails with the necessary information.
That's what the reversible method is for:
def change
reversible do |dir|
dir.up do
change_column :users, :score, :decimal, precision: 6, scale: 2
end
dir.down do
change_column :users, :score, :decimal, precision: 4, scale: 2
end
end
end
At first glance, this seems more complicated than using up and down. The advantage is, that you can mix it with reversible migrations.
If i.e. you add a bunch of fields to a table and have to change few fields along, you can use change_table and add_column together with reversible to have a clean and compact migration:
def change
change_table :users do |t|
t.string :address
t.date :birthday
# ...
end
reversible do |dir|
dir.up do
change_column :users, :score, :decimal, precision: 6, scale: 2
end
dir.down do
change_column :users, :score, :decimal, precision: 4, scale: 2
end
end
end
It's possible rollback a migration with change_columns in rails 4 or
rails 5 with:
def change end
Yes, it is possible to rollback a migration with empty (as well as correct non-empty) change method.
When you actually have something there, you might not be able to revert a migration with for example, a remove_column if you did not specify the type of the column you removed.
Also, if for example you have a change_column in change method. How should Rails know, which type/name/whatever the column had before the change if not from down method? :)
So if you are planning on rolling back and forth, you would want to be as explicit, as possible, so using up and down is a very good (best, actually) idea.
You can also use
change_column_null(table_name, column_name, null, default = nil)
To save some code. Activerecord knows how to rollback this one.
EDIT: I just want to apologize for taking so long to update this answer, I originally didn’t understand the question.
When you are writing a migration in Rails you have two approaches you can take you can use change or the up down approach. When you use change it is important to be aware of what your migration is doing, because the change construct will attempt to infer what the opposite action would be in the event you have to rollback the migration. Migration methods like add_column, add_index, create_table, etc, have opposite actions rails can infer. A migration like change_column however can be literally anything so like Martin said you would need to include the reversible block for rails to know what to do when the revert could be ambiguous.
I have a rails model that has a non-defaulted boolean field that is nullable and am trying to set a default. I found a blog post about avoiding a 3-stat boolean issue, so am trying to cater for this. This is the migration I have:
def change
change_column :table, is_foo, :boolean, null: false, default: false
end
Running the migration fails because of existing null values in the database. What is the correct way to update existing entries to allow the schema change? Or should the not null control be added to the model:
validates :is_foo, presence: true
Not sure if adding this to the migration is the "right" way:
Table.update_all(:is_foo => false)
Similarly, this field was added by a migration without extra not null / default parameters. Would the migration to add column also require this, or would the default set the value? Here's the migration I ran:
add_column :table, is_foo, :boolean
If I had added ,null: false, default: false on the add_column, would all the values have been set correctly?
You can actually combine the change_column_null and change_column_default methods to accomplish this in a migration.
The change_column_null method lets you add a NOT NULL constraint, with the last argument specifying what to replace any existing NULL values with.
The change_column_default then sets the default for any new records.
class UpdateTable < ActiveRecord::Migration
def change
change_column_null :table, :is_foo, false, false
change_column_default :table, :is_foo, false
end
end
You could do it this way:
class UpdateFoo < ActiveRecord::Migration
def change
reversible do |direction|
direction.up {
Table.where(is_foo: nil).update_all(is_foo: false)
}
end
change_column :table, :is_foo, :boolean, null: false, default: false
end
end
When migrating up it will ensure first that all nulls are converted to false and then change the column to add your restrictions.
And yes, you could have avoided this if the first migration contained the restrictions.
I think you are also right adding the model validation.
Let's say I create a table in a Rails migration, specifying to omit the ID column:
create_table :categories_posts, :id => false do |t|
t.column :category_id, :integer, :null => false
t.column :post_id, :integer, :null => false
end
Later I decide I want to add an ID column as a primary key so I create a new migration:
class ChangeCategoriesToRichJoin < ActiveRecord::Migration
def self.up
add_column :categories_posts, :id, :primary_key
end
def self.down
remove_column :categories_posts, :id
end
end
But when I look at the table after I migrate, it looks like this:
category_id
post_id
id
The id column is in the last position in the table, whereas normally an id column would be first.
Is there a way to change the ChangeCategoriesToRichJoin migration to insist on the id column being created BEFORE the category_id column in the table?
Or do I need to drop the table and add the column in the "create table" definition?
Use :after => :another_column_name, e.g.:
change_table :users do |t|
t.integer :like_count, :default => 0, :after => :view_count
end
I haven't been able to put the columns in order, myself, but with Rails you can rollback, alter the old migration file to add the new columns in the order you want, then re-migrate up the old migration including the new field. It's not exactly ideal, but the ability to migrate and rollback easily, it can work if you're OCD enough to require column order. :P
I am not an expert, but I've read a lot of Rails documentation (and very recently) and can't recall finding a solution for this.