How to write conditional migrations in rails? - ruby-on-rails

I am looking for ways to write migrations in rails that can be executed against the database many times without failing.
For instance let say I have this migration:
class AddUrlToProfile < ActiveRecord::Migration
def self.up
add_column :profile, :url, :string
end
def self.down
remove_column :profile, :url
end
end
If the url column already exists in the Profile table (if the schema.rb has been modified unexpectedly for instance), my migration will fail saying that it's a duplicate!
So how to execute this migration only if it has to?
Thanks

You can do something like this:
class AddUrlToProfile < ActiveRecord::Migration
def self.up
Profile.reset_column_information
add_column(:profile, :url, :string) unless Profile.column_names.include?('url')
end
def self.down
Profile.reset_column_information
remove_column(:profile, :url) if Profile.column_names.include?('url')
end
end
This will reset the column information before it begins - making sure that the Profile model has the up-to-date column information from the actual table. It will then only add the column if it doesn't exist. The same thing happens for the down function, but it only removes the column if it exists.
If you have multiple use cases for this you could factor the code out into a function and re-use that in your migrations.

For Rails 3.X, there's the column_exists?(:table_name, :column_name) method.
For Rails 2.X, you can check the existence of columns with the following:
columns("<table name>").index {|col| col.name == "<column name>"}
...or if you're not in a migration file:
ActiveRecord::Base.connection.columns("<table name>").index {|col| col.name == "<column name>"}
If it returns nil, no such column exists. If it returns a Fixnum, then the column does exist. Naturally, you can put more selective parameters between the {...} if you want to identify a column by more than just its name, for example:
{ |col| col.name == "foo" and col.sql_type == "tinyint(1)" and col.primary == nil }

This should work
def self.table_exists?(name)
ActiveRecord::Base.connection.tables.include?(name)
end
if table_exists?(:profile) && !Profile.column_names.include?("url")
add_column :profile, :url, :string
end

Wrapping my migration in a conditional worked for me.
Rails 4.X
class AddUrlToProfile < ActiveRecord::Migration
unless Profile.column_names.include?("url")
def self.up
add_column :profile, :url, :string
end
def self.down
remove_column :profile, :url
end
end
end

Related

Migrate from two string yes/no variable to a single boolean variable in rails?

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

Rails default to a defined value

Hello I was wondering what the correct way to a defined default value as in
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :type, default: "Your value"
end
end
but instead of "Your Value" it was #yourvalue which is stored in a #settings model
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :type, default: #settings.yourvalue
end
end
class CreateSettings < ActiveRecord::Migration[5.2]
def change
create_table
t.string :yourvalue
t.timestamps
end
end
end
#yourvalue would be defined and updated by a user
Migrations are normal Ruby classes. You can write Rails code within them. Eg. you can access your models.
class SetDefault < ActiveRecord::Migration
def change
change_column :table_name, :column_name, :string, default: Setting.find(1).yourvalue
end
end
Replace Setting.find(1) with whatever logic you need to filter.
I would set it on the model so your migrations remain non-reliant on previous states of the data therein.
class TableName < ApplicationRecord
before_create :add_defaults
# whatever
private
def add_defaults
self.column_name = Setting.find(1).default_value unless column_name
end
end
This way you won't be invoking ActiveRecord from the migration, and your app will still work if you deploy it somewhere and run the migrations before adding a Setting record. You'll likely need a fully-migrated database before adding any data.

making the default value for a migration based upon a calculation using two other columns

I'm adding a column to a model and I want the new column's default to be the sum of two of the existing columns in that table. Not sure if this is possible. Here's the (invalid) migration I tried:
class AddRoundsWithSpeaksToCases < ActiveRecord::Migration
def change
add_column :cases, :rounds_with_speaks, :integer, default: :wins + :losses
end
end
Is there a way to do this via a migration, or do I have to run rails console and update the attributes through there?
You wouldn't want to set the migration up that way because :wins + :losses is irrelevant for a new Case record. The default should be 0, since by "default" the value of the field will be 0 for new records.
Additionally, you don't have to rely on the console to run the update, you can put it in the migration:
class AddRoundsWithSpeaksToCases < ActiveRecord::Migration
def up
add_column :cases, :rounds_with_speaks, :integer, default: 0
Case.find_each(batch_size: 200) { |c| c.update_attribute!(:rounds_with_speaks, c.wins + c.losses) }
end
def down
remove_column :cases, :rounds_with_speaks
end
end
As a one time migration, to the historical records, you can do it like this:
class AddRoundsWithSpeaksToCases < ActiveRecord::Migration
def change
add_column :cases, :rounds_with_speaks, :integer
update "UPDATE cases SET rounds_with_speaks = wins+losses"
end
end
This won't affect any future records, you could write something like these as a SQL trigger/procedure..

Changing column names with paperclip gem

Is there a way to change the column names that paperclip uses when generating its database migration? For example, currently a paperclip migration looks like this:
class AddAvatarColumnsToUsers < ActiveRecord::Migration
def self.up
add_attachment :users, :avatar
end
def self.down
remove_attachment :users, :avatar
end
end
And it generates the following in the database:
avatar_file_name
avatar_file_size
avatar_content_type
avatar_updated_at
Ideally I'd like to change the avatar_file_name to correspond to a column in the database called "content". Is this possible?
Renaming columns, to something else than <attachment>_<attribute> will not work in Paperclip.
It mandatorily needs the following 4 attributes for each attachment in the model:
<attachment>_file_name
<attachment>_file_size
<attachment>_content_type
<attachment>_updated_at
Check this post out for a debate on why following a <attachment>_url approach might be better than having a separate model for attachment (<attachment>.url): Paperclip and Inheritance (STI)
For users that leverage S3 or Google Cloud Storage, you may need to move your files to a new directory following on paperclip's file naming scheme. Here is a sample migration to point you in the right direction:
class MovePaperclipAssets < ActiveRecord::Migration
def up
storage = Fog::Storage::Google.new google_storage_access_key_id: ENV["GOOGLE_ACCESS_KEY_ID"],
google_storage_secret_access_key: ENV["GOOGLE_SECRET_ACCESS_KEY"]
bucket = storage.directories.get('bucket-name')
bucket.files.each do |file|
if file.key.starts_with?("original/directory")
newfile = file.key.gsub("original/directory","new/directory")
file.copy('bucket-name', newfile)
file.destroy
end
end
end
def down
storage = Fog::Storage::Google.new google_storage_access_key_id: ENV["GOOGLE_ACCESS_KEY_ID"],
google_storage_secret_access_key: ENV["GOOGLE_SECRET_ACCESS_KEY"]
bucket = storage.directories.get('bucket-name')
bucket.files.each do |file|
if file.key.starts_with?("new/directory")
newfile = file.key.gsub("new/directory","original/directory")
file.copy('bucket-name', newfile)
file.destroy
end
end
end
end
ruby script/generate migration RenameDatabaseColumn
class RenameDatabaseColumn < ActiveRecord::Migration
def self.up
rename_column :user, :avatar_file_name, :user_file_name
rename_column :user, :avatar_file_size, :user_file_size
rename_column :user, :avatar_content_type, :user_content_type
rename_column :user, :avatar_updated_at, :user_updated_at
end
def self.down
# rename back if you need or do something else or do nothing
end
end
Just try the migration may be it will work for you. add column name whatever corresponding column name.
When you run the rails g migration add_avatar_columns_to_users, you can write as you wrote. But if you want to change the column names, go to the migration file and change them as you want as following. Please don't migrate it untill and unless you change the columns.
class AddAvatarColumnsToUsers < ActiveRecord::Migration
def self.up
add_column :users, :your_file_name,:string
add_column :users, :your_content_type, :string
add_column :users, :your_file_size,:integer
add_column :users, :yourfile_updated_at,:datetime
end
def self.down
remove_column :users, :your_file_name
remove_column :users, :your_content_type
remove_column :users, :your_file_size
remove_column :users, :yourfile_updated_at
end
end
Now run rake db:migrate
The above will work for sure. And add your attachments accordinbly.

Alter existing model with a Redmine plugin

The Redmine plugin tutorials explain how to wrap core models but what I need is to add another column to the journals table.
I need a boolean field inserted in the journals model. Creating another model with a 'belongs_to :journal' relation seems like an overkill.
Can this be done with a plugin?
I should note that I am a rails newbie.
You just have to create the appropriate migration.
In your plugin's directory, create the file db/migrate/update_journal.rb with the following :
class UpdateJournal < ActiveRecord::Migration
def self.up
change_table :journal do |t|
t.column :my_bool, :boolean
end
end
def self.down
change_table :journal do |t|
t.remove :my_bool
end
end
end
Then you can execute the task rake db:migrate_plugins RAILS_ENV=production to update your database with the new field.
After executing the migration, your journal database will have the my_bool field that you'll be able to call like every other field.
I was able to extend the existing user model using the following code:
class UpdateUsers < ActiveRecord::Migration
def up
add_column :users, :your_new_column, :string, :default => ''
add_column :users, :your_other_new_column, :string, :default => ''
end
def down
remove_column :users, :your_new_column
remove_column :users, :your_other_new_column
end
end
Also I needed to name the migration file in way that it began with a number eg. myplugin/db/migrate/001_update_user.rb

Resources