reversible and revert in Active Record migrations - ruby-on-rails

I have looked at Rails Guides and the Rails API and I can't understand the usage of reversible and revert.
So for example, see the example linked here http://guides.rubyonrails.org/migrations.html#using-reversible and included below:\
It says
Complex migrations may require processing that Active Record doesn't know how to reverse. You can use reversible to specify what to do when running a migration what else to do when reverting it. For example,
class ExampleMigration < ActiveRecord::Migration
def change
create_table :products do |t|
t.references :category
end
reversible do |dir|
dir.up do
#add a foreign key
execute <<-SQL
ALTER TABLE products
ADD CONSTRAINT fk_products_categories
FOREIGN KEY (category_id)
REFERENCES categories(id)
SQL
end
dir.down do
execute <<-SQL
ALTER TABLE products
DROP FOREIGN KEY fk_products_categories
SQL
end
end
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
I get that the code in the down section is what will be run on rollback, but why include the code in the up block? I have also seen another example which had a reversible section with only an up block. What would the purpose of such code be?
Finally, I do not understand revert. Below is the example included in the Rails Guides, but it makes little sense to me.
`The revert method also accepts a block of instructions to reverse. This could be useful to revert selected parts of previous migrations. For example, let's imagine that ExampleMigration is committed and it is later decided it would be best to serialize the product list instead. One could write:
class SerializeProductListMigration < ActiveRecord::Migration
def change
add_column :categories, :product_list
reversible do |dir|
dir.up do
# transfer data from Products to Category#product_list
end
dir.down do
# create Products from Category#product_list
end
end
revert do
# copy-pasted code from ExampleMigration
create_table :products do |t|
t.references :category
end
reversible do |dir|
dir.up do
#add a foreign key
execute <<-SQL
ALTER TABLE products
ADD CONSTRAINT fk_products_categories
FOREIGN KEY (category_id)
REFERENCES categories(id)
SQL
end
dir.down do
execute <<-SQL
ALTER TABLE products
DROP FOREIGN KEY fk_products_categories
SQL
end
end
# The rest of the migration was ok
end
end
end`

I'm not an expert in this by any means, but my understanding from reading the guides is as follows:
The reversible call in the first example is expressing the second of four components of the change migration. (Note: The indentation you have is misleading in this regard and should probably be updated to match the guide.) It is related to but distinct from the other components, so it makes sense that it would have both an up and down section. I can't explain why you would have reversible with only one direction and not the other, as you indicated you'd seen.
The revert call is telling the system to revert a previous migration either by name or by providing a block describing the (forward) migration. The example you showed is the latter case and I think is best understood by careful reading of the paragraph that follows it in the guide, to wit:
The same migration could also have been written without using revert
but this would have involved a few more steps: reversing the order of
create_table and reversible, replacing create_table by drop_table, and
finally replacing up by down and vice-versa. This is all taken care of
by revert.

Related

How to Delete Table from Database permanently in ROR

I've generated a scaffold and then migrated my database. After that, I destroyed my scaffolding and then generated scaffold again and try to migrate my database but I got an error that this table already exists. How can I delete that table from the database?
One other easy way to do so :
open rails console and use this command :
ActiveRecord::Migration.drop_table(:your_table_name)
If you haven't pushed your code then the solution given by #Cryptex Technologies works fine. But if you have(i.e if you are using version control) then i won't recommend that approach. In that case you should create a new migration something like this:
class RemoveTable < ActiveRecord::Migration[5.2]
def up
drop_table :table_name
end
def down
create_table :table_name do |t|
t.string :field_name_1
t.text :field_name_2
t.timestamps
end
add_index :table_name, :field_name_1, unique: true
end
end
To delete that table, you need to run the command
rake db:rollback
and if you want to delete the database of your current application.
then you need to run the command
rake db:drop.
Rails automatically generates migration, thanks to the command line generator.
For instance, if you want to remove the Users Table write a command line statement like this:
rails generate migration DropUsersTable
This will generate the empty .rb file in /db/migrate/ that still needs to be filled to drop the “Users” table in this case.
A Quick-and-Dirty™ implementation would look like this:
class DropUsersTable < ActiveRecord::Migration
def up
drop_table :users
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
It’s “correct” as it shows that the migration is one-way only and should not/can not be reversed. But in order to make a truly clean job in case these modifications were to be reversed we need to have a symmetrical migration (assuming we could recover the lost data), which we can do by declaring all the fields of our table in the migration file:
class Dropusers < ActiveRecord::Migration
def change
drop_table :users do |t|
t.string :name, null: false
t.timestamps null: false
end
end
end
This can be long if the model is complex, but it ensures full reversibility.
Here again, changes will enter effect as usual after running rake db:migrate.

add_foreign_key not creating foreign keys

Im using Rails 5.1 and SQLite. The below migration is not working as expected.
class AddJobTitleForeignKeyToTimeOffTypes < ActiveRecord::Migration[5.1]
def change
add_column :time_off_types, :job_title_id, :integer
add_foreign_key :time_off_types, :job_title, :column => :job_title_id
end
end
It creates the column "job_title_id" in the table "time_off_types" but it does not create the foreign key.
ActiveRecord's add_foreign_key method is used outside table creation, hence uses ALTER TABLE ... ADD CONSTRAINT ....
SQLite's ALTER TABLE does not support adding constraints (of any kind). (It is worth reviewing, as ALTER TABLE in SQLite might be more limited than you expect. For example, SQLite < 3.25.0 cannot rename columns either.)
However, SQLite CREATE TABLE does support foreign key constraints and the ActiveRecord migration create_table #references method can create them:
def change
create_table :pets do
t.references :owner, foreign_key: true
...
end
end
The Rails Migrations Guide does not mention this difference.
So how does this work?
ActiveRecord database adapters have two methods: supports_foreign_keys? and supports_foreign_keys_in_create? which are both false by default (see the Rails API documentation).
add_foreign_key returns immediately unless supports_foreign_keys? is true, but it is false for SQLite, so end of the trail for add_foreign_key.
On the other hand, supports_foreign_keys_in_create? is true for SQLite >= 3.6.19, which allows the #references method to create the foreign key using CREATE TABLE ....
(I've linked to Rails 5.1 code, as that's what you were using at the time of the question, but this all remains true as of Rails 5.2.1 today.)

index name too long to rollback migration

I'm on a git branch and trying to rollback two migrations, before destroying the branch. The most recent migration adds a column to a table that I want to keep (and which is part of master, not the branch to be dropped), so dropping the whole table is not a solution (unless I have to recreate it again). Anyways, I must have done something wrong, because when I tried to remove the apple_id column from the scores table, I got this abort error.
This is the migration that I'm trying to rollback
add_column :scores, :apple_id, :integer
However, the error message (see below) is referring to the indexes that I created with the original migration (part of master branch) that created the table
add_index :scores, [:user_id, :created_at, :funded, :started]
Can you suggest what I might do?
== AddAppleidColumnToScores: reverting =======================================
-- remove_column("scores", :apple_id)
rake aborted!
An error has occurred, this and all later migrations canceled:
Index name 'temp_index_altered_scores_on_user_id_and_created_at_and_funded_and_started' on table 'altered_scores' is too long; the limit is 64 characters
Update: reading this SO question How do I handle too long index names in a Ruby on Rails migration with MySQL?, I got some more information about the source of the problem but don't know how to solve it. Both sql and postgres have 64 character limits
Index name 'index_studies_on_user_id_and_university_id_and_subject_\
name_id_and_subject_type_id' on table 'studies' is too long; \
the limit is 64 characters
The accepted answer for the question I refer to says to give the index a name, although I'm not sure how I could do this now that I'm trying to rollback.
add_index :studies, ["user_id", "university_id", \
"subject_name_id", "subject_type_id"],
:unique => true, :name => 'my_index'
Update: in response to the comments, I'm using Rails 3.2.12. This is the migration that adds the column
class AddAppleidColumnToScores < ActiveRecord::Migration
def change
add_column :scores, :apple_id, :integer
end
end
Furthermore, the reason why I didn't want to drop the table was that I was unsure about what problems it might cause in recreating it since a) the main part was created on branch master, while a column added on a branch and b) I was unsure about what to do with the migration file for the dropped table? since it was the fourth (of about 10) tables I created, I don't know how to run it and only it again.
Can you copy/paste your migration?
Here's mine:
class AddAppleIdColumnToScores < ActiveRecord::Migration
def self.up
add_column :scores, :apple_id, :integer
add_index :scores, [:user_id, :created_at, :funded, :started]
end
def self.down
# delete index MySql way
# execute "DROP INDEX index_scores_on_user_id_and_created_at_and_funded_and_started ON scores"
# delete index Postgresql way
# execute "DROP INDEX index_scores_on_user_id_and_created_at_and_funded_and_started"
# delete index Rails way / not necessary if you remove the column
# remove_index :scores, [:user_id, :created_at, :funded, :started]
remove_column :scores, :apple_id
end
end

Difference between foreign key constraint and references in Rails

Is there any difference between using t.references and executing SQL command to create foreign key relationship between products and category table as shown below? In other words, are the two different ways of doing the same thing or am I missing anything here?
class ExampleMigration < ActiveRecord::Migration
def up
create_table :products do |t|
t.references :category
end
#add a foreign key
execute <<-SQL
ALTER TABLE products
ADD CONSTRAINT fk_products_categories
FOREIGN KEY (category_id)
REFERENCES categories(id)
SQL
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
def down
rename_column :users, :email_address, :email
remove_column :users, :home_page_url
execute <<-SQL
ALTER TABLE products
DROP FOREIGN KEY fk_products_categories
SQL
drop_table :products
end
end
They're not the same thing. Rails by default doesn't enforce foreign keys in the database. Instead, references when creating from the command line also creates a regular index, like this:
add_index :products, :category_id
Update:
Rails 5 actually does exactly the same thing now. So, to answer the original question: Nowadays, both are the same.
I found some thing intresting in this page.
http://railsforum.com/viewtopic.php?id=17318
From a comment :
Rails doesn't use foreign keys to perform his backend tasks. This
because some db like sqlite doesn't allow foreign keys on its tables.
So Rails doesn't provide an helper to build a foreign key
Also there is a gem foreigner for adding foreign keys to database table.
What creates the FOREIGN KEY constraint in Ruby on Rails 3?

Cannot add a NOT NULL column with default value NULL in Sqlite3

I am getting the following error while trying to add a NOT NULL column to an existing table. Why is it happening ?. I tried rake db:reset thinking that the existing records are the problem, but even after resetting the DB, the problem persists. Can you please help me figure this out.
Migration File
class AddDivisionIdToProfile < ActiveRecord::Migration
def self.up
add_column :profiles, :division_id, :integer, :null => false
end
def self.down
remove_column :profiles, :division_id
end
end
Error Message
SQLite3::SQLException: Cannot add a NOT NULL column with default value NULL: ALTER TABLE "profiles" ADD "division_id" integer NOT NULL
This is (what I would consider) a glitch with SQLite. This error occurs whether there are any records in the table or not.
When adding a table from scratch, you can specify NOT NULL, which is what you're doing with the ":null => false" notation. However, you can't do this when adding a column. SQLite's specification says you have to have a default for this, which is a poor choice. Adding a default value is not an option because it defeats the purpose of having a NOT NULL foreign key - namely, data integrity.
Here's a way to get around this glitch, and you can do it all in the same migration. NOTE: this is for the case where you don't already have records in the database.
class AddDivisionIdToProfile < ActiveRecord::Migration
def self.up
add_column :profiles, :division_id, :integer
change_column :profiles, :division_id, :integer, :null => false
end
def self.down
remove_column :profiles, :division_id
end
end
We're adding the column without the NOT NULL constraint, then immediately altering the column to add the constraint. We can do this because while SQLite is apparently very concerned during a column add, it's not so picky with column changes. This is a clear design smell in my book.
It's definitely a hack, but it's shorter than multiple migrations and it will still work with more robust SQL databases in your production environment.
You already have rows in the table, and you're adding a new column division_id. It needs something in that new column in each of the existing rows.
SQLite would typically choose NULL, but you've specified it can't be NULL, so what should it be? It has no way of knowing.
See:
Adding a Non-null Column with no Default Value in a Rails Migration (2009, no longer available, so this is a snapshot at archive.org)
Adding a NOT NULL Column to an Existing Table (2014)
That blog's recommendation is to add the column without the not null constraint, and it'll be added with NULL in every row. Then you can fill in values in the division_id and then use change_column to add the not null constraint.
See the blogs I linked to for an description of a migration script that does this three-step process.
If you have a table with existing rows then you will need to update the existing rows before adding your null constraint. The Guide on migrations recommends using a local model, like so:
Rails 4 and up:
class AddDivisionIdToProfile < ActiveRecord::Migration
class Profile < ActiveRecord::Base
end
def change
add_column :profiles, :division_id, :integer
Profile.reset_column_information
reversible do |dir|
dir.up { Profile.update_all division_id: Division.first.id }
end
change_column :profiles, :division_id, :integer, :null => false
end
end
Rails 3
class AddDivisionIdToProfile < ActiveRecord::Migration
class Profile < ActiveRecord::Base
end
def change
add_column :profiles, :division_id, :integer
Profile.reset_column_information
Profile.all.each do |profile|
profile.update_attributes!(:division_id => Division.first.id)
end
change_column :profiles, :division_id, :integer, :null => false
end
end
You can add a column with a default value:
ALTER TABLE table1 ADD COLUMN userId INTEGER NOT NULL DEFAULT 1
The following migration worked for me in Rails 6:
class AddDivisionToProfile < ActiveRecord::Migration[6.0]
def change
add_reference :profiles, :division, foreign_key: true
change_column_null :profiles, :division_id, false
end
end
Note :division in the first line and :division_id in the second
API Doc for change_column_null
Not to forget that there is also something positive in requiring the default value with ALTER TABLE ADD COLUMN NOT NULL, at least when adding a column into a table with existing data. As documented in https://www.sqlite.org/lang_altertable.html#altertabaddcol:
The ALTER TABLE command works by modifying the SQL text of the schema
stored in the sqlite_schema table. No changes are made to table
content for renames or column addition. Because of this, the execution
time of such ALTER TABLE commands is independent of the amount of data
in the table. They run as quickly on a table with 10 million rows as
on a table with 1 row.
The file format itself has support for this https://www.sqlite.org/fileformat.html
A record might have fewer values than the number of columns in the
corresponding table. This can happen, for example, after an ALTER
TABLE ... ADD COLUMN SQL statement has increased the number of columns
in the table schema without modifying preexisting rows in the table.
Missing values at the end of the record are filled in using the
default value for the corresponding columns defined in the table
schema.
With this trick it is possible to add a new column by updating just the schema, operation that took 387 milliseconds with a test table having 6.7 million rows. The existing records in the data area are not touched at all and the time saving is huge. The missing values for the added column come on-the-fly from the schema and the default value is NULL if not otherwise stated. If the new column is NOT NULL then the default value must be set to something else.
I do not know why there is not a special path for ALTER TABLE ADD COLUMN NOT NULL when the table is empty. A good workaround is perhaps to create the table right from the beginning.

Resources