Can I rollback using change_column method in Rails 4 and above? - ruby-on-rails

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.

Related

Add timestamps to existing table in db Rails 5+

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

How change column type works in rails migration

We know, rails ActiveRecord::Migration now have a new method
def change
add_column :accounts, :name, :string
add_index :accounts, :name
change_column :my_table, :some_id, :string
end
But my question is for
change_column :my_table, :some_id, :string
rails do not need to know :some_id's previous type is integer or not.
For example assume :some_id was an integer, after this migration it is converted to string.
when I revert this migration :some_id type should be integer again. Am i right ?? but how can rails understand :some_id previous type was integer.
in previous mehtod self.up and self.down it is written in migration file. so it was not problem. rails can easily find that. but in change method how it is recollected?? Does it check the migration for this table in previous migration files where last data type was definded for :some_id or anything else ??
The change_column is an irreversible migration method. So you cannot reverse this migration using change method. To do this you need to write up and down methods. If you just write this in change method, when you run
rake db:rollback
it will throw this exception
ActiveRecord::IrreversibleMigration
You can read more more on:
http://edgeguides.rubyonrails.org/active_record_migrations.html#changing-columns

method names in Rails migrations

I'm using Agile Web Development with Rails to learn about Rails. In an early chapter, the author created scaffolding and then started looking at the Migration. In his Migration, there is an "up" and and "down" method, whereas I only have a "change" method in my Migration. The author is using Rails 3.05 (or something like that) and I am using 3.1, however, I don't think that's the explanation, because using another book but same version of Rails I remember creating a migration that had the "up" and "down" methods...
So, two questions,
a) What's the reason why I have different method names in my migration?
b) is it going to affect functionality?
My Migration
class CreateProducts < ActiveRecord::Migration
def change
create_table :products do |t|
t.string : title
t.text :description
t.string :image_url
t.decimal :price, :precision => 8, :scale => 2
t.timestamps
end
end
end
Books Migration
class CreateProducts < ActiveRecord::Migration
def self.up
create_table :products do |t|
t.string :title
t.text :description
t.string :image_url
t.decimal :price, :precision => 8, :scale => 2
t.timestamps
end
end
def self.down
drop_table :products
end
end
Rails 3.1 did away with both the "up" and "down" part of migrations. Now they are called "reversible migrations" that use the change method. So your first code example is correct for Rails 3.1, second is correct for 3.0.x and earlier. Here are the change notes for 3.1 that skim over this update:
https://gist.github.com/958283
The important line: Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's change method instead of the ordinary up and down methods.
The update makes sense if you think about it... you no longer have to define all the steps to "up" your database as well as type out the same steps in reverse to "down" your database. The change method is smart enough to go back and forth given the singe set of instructions.
To answer your second question, no it won't change how the migration works. It will still update your data store per your instructions, keep track of the migration, etc. It's just a more efficient way of describing those changes to your Model.
Same code really, just more dry (and slightly less customizeable).
Theres a good description here:
http://edgerails.info/articles/what-s-new-in-edge-rails/2011/05/06/reversible-migrations/index.html

Adding a Model Reference to existing Rails model

I'd like to know the "proper" way to approach adding a relation between two existing classes in Rails 3.
Given existing models: Clown & Rabbit
I'd like to add a reference (belongs_to) from Rabbit to Clown. I start by trying to generate a migration:
rails g migration AddClownToRabbits clown:reference
which gives me a migration that looks like:
class AddClownToRabbits < ActiveRecord::Migration
def self.up
add_column :rabbits, :clown, :reference
end
def self.down
remove_column :rabbits, :clown
end
end
After rake db:migrate on this migration I examine SQLite3's development.db and see a new column: "clown" reference
I guess I was expecting a "clown_id" integer column and a migration that looked like:
class AddClownToRabbits < ActiveRecord::Migration
def self.up
add_column :rabbits, :clown_id
end
def self.down
remove_column :rabbits, :clown_id
end
end
I'm sure :reference is supposed to be equivalent to "t.references :clown" but I can't find the documentation (big surprise). API says add_column: Instantiates a new column for the table. The type parameter is normally one of the migrations native types, which is one of the following: :primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.
...with no reference to :reference.
If you are using edge rails (4.0) you can use:
rails generate migration AddAddressRefToContacts address:references
As you can see by the docs.
After you set belongs_to in Rabbit, and has_many in Clown, you can do a migration with:
add_column :rabbit, :clown_id, :integer
EDIT: See Paulo's answer below for a more updated answer (Rails 4+)
I'm not sure where you got this idea, but there is no (and never has been) such syntax to do what you want with add_column. To get the behavior you want, you'd have to do t.refences :clown, as you stated. In the background this will call: #base.add_column(#table_name, "#{col}_id", :integer, options).
See here.
EDIT:
I think I can see the source of your confusion. You saw the method call t.reference and assumed it was a datatype because calls such as t.integer and t.string exist, and those are datatypes. That's wrong. Reference isn't a datatype, it's just simply the name of a method, similar to t.rename is.

Rails migrations: Undo default setting for a column

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

Resources