Change multiple columns with one single migration change - ruby-on-rails

I have the following migration. Is there a way to run these changes in one change than 3?
def change
change_column :comments, :attr_1, :string, null: true
change_column :comments, :attr_2, :string, null: true
change_column :comments, :attr_3, :string, null: true
end

The short answer is no. The change_column method is configured to take arguments for table name, column name, and an options hash. The source code for change_column can be found here: https://github.com/rails/rails/blob/0fe76197d2622674e1796a9a000995a7a1f6622b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
The only line in the change_column method is:
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
which executes an ALTER script on the table name passed as the first argument. You can't pass an array of tables/columns as arguments, so you have to do them one at a time. This paradigm is fairly typical in Rails migrations.

You can use the change_table function with bulk set to true which will run the alter as a single MySQL query and be much faster than running individually. Example is below.
def change
change_table(:comments, bulk: true) do |t|
t.change :attr_1, :string, null: true
t.change :attr_2, :string, null: true
t.change :attr_3, :string, null: true
end
end

Related

Rails: remove array from DB column in migration

In Rails, you can add an array column in a migration like so:
change_table :scheduled_posts do |t|
t.string :list_id, array: true
end
What's the syntax to change it to a non-array?
Turns out it was pretty simple:
change_column :scheduled_posts, :list_id, :string, array: false

Uniqueness with scope in migration

I've been trying to find a way to achieve this but I cannot find any attempts even so I am thinking that maybe my approach is completely wrong. That said, what should I do in my migration if I want a combination of two fields to be unique? Please note that I do not want them to be indexes, just database fields.
For example, for the migration below, I can separately add unique: true to the fields, but the combo?
class CreateSomething < ActiveRecord::Migration
def change
create_table :something do |t|
t.date :datestamp, :null => false
t.integer :some_number, :null => false
t.timestamps
end
end
end
I'm not sure what you mean by
Please note that I do not want them to be indexes, just database fields.
Indexes are extra pieces on information that the database stores about the columns.
More importantly an index is exactly what you need!
class CreateSomething < ActiveRecord::Migration
def change
create_table :something do |t|
t.date :datestamp, :null => false
t.integer :some_number, :null => false
t.timestamps
end
add_index :something, [:datestamp, :some_number], unique: true
end
end

Add data to a unique, required column

I'm looking for the preferred way, and unique column to an existing table. I also want to add a unique index to the table. Before adding the index though, I obviously need to add data to the column to prevent the index creation from failing.
Here is the situation:
class AddUsernameToUsers < ActiveRecord::Migration
def change
add_column :users, :username, :string, null: false
# Need Data here! And don't want to do something like this:
# User.each { |u| u.update_attribute(:username, u.email }
add_index :users, :username, unique: true
end
end
I know using ruby code to populate the data is possible, there are lots of examples of that, but I keep reading that it isn't such a good idea. Are there any options other than something similar to the above?
would the following work for your situation?
class AddUsernameToUsers < ActiveRecord::Migration
def change
add_column :users, :username, :string, null: true
execute("UPDATE users SET username = email")
change_column :users, :username, :string, null: false
add_index :users, :username, unique: true
end
end

How to update reference column in Ruby

I have a migration that I want to make with a reference in my table. I create the reference using this:
create_table :user_events do |t|
t.references :user, :null => false
end
And in my migration, I want to be able to allow the reference to be NULL.
def self.up
change_column :user_events, :user, :integer, :null => true
end
However I keep getting PGError: ERROR: column "user" of relation "user_events" does not exist. Am I migrating wrong?
This should work:
def self.up
change_column :user_events, :user_id, :integer, :null => true
end
Note that the column you're trying to change is called user_id, not user
It's because your migration creates a column named user_id, referencing the User model.
try
def self.up
change_column :user_events do |c|
c.references :user, :integer, :null => true
end
end

Add timestamps to an existing table

I need to add timestamps (created_at & updated_at) to an existing table. I tried the following code but it didn't work.
class AddTimestampsToUser < ActiveRecord::Migration
def change_table
add_timestamps(:users)
end
end
The timestamp helper is only available in the create_table block. You can add these columns by specifying the column types manually:
class AddTimestampsToUser < ActiveRecord::Migration
def change_table
add_column :users, :created_at, :datetime, null: false
add_column :users, :updated_at, :datetime, null: false
end
end
While this does not have the same terse syntax as the add_timestamps method you have specified above, Rails will still treat these columns as timestamp columns, and update the values normally.
Migrations are just two class methods (or instance methods in 3.1): up and down (and sometimes a change instance method in 3.1). You want your changes to go into the up method:
class AddTimestampsToUser < ActiveRecord::Migration
def self.up # Or `def up` in 3.1
change_table :users do |t|
t.timestamps
end
end
def self.down # Or `def down` in 3.1
remove_column :users, :created_at
remove_column :users, :updated_at
end
end
If you're in 3.1 then you could also use change (thanks Dave):
class AddTimestampsToUser < ActiveRecord::Migration
def change
change_table(:users) { |t| t.timestamps }
end
end
Perhaps you're confusing def change, def change_table, and change_table.
See the migration guide for further details.
#user1899434's response picked up on the fact that an "existing" table here could mean a table with records already in it, records that you might not want to drop. So when you add timestamps with null: false, which is the default and often desirable, those existing records are all invalid.
But I think that answer can be improved upon, by combining the two steps into one migration, as well as using the more semantic add_timestamps method:
def change
add_timestamps :projects, default: Time.zone.now
change_column_default :projects, :created_at, nil
change_column_default :projects, :updated_at, nil
end
You could substitute some other timestamp for DateTime.now, like if you wanted preexisting records to be created/updated at the dawn of time instead.
Your original code is very close to right, you just need to use a different method name. If you're using Rails 3.1 or later, you need to define a change method instead of change_table:
class AddTimestampsToUser < ActiveRecord::Migration
def change
add_timestamps(:users)
end
end
If you're using an older version you need to define up and down methods instead of change_table:
class AddTimestampsToUser < ActiveRecord::Migration
def up
add_timestamps(:users)
end
def down
remove_timestamps(:users)
end
end
class AddTimestampsToUser < ActiveRecord::Migration
def change
change_table :users do |t|
t.timestamps
end
end
end
Available transformations are
change_table :table do |t|
t.column
t.index
t.timestamps
t.change
t.change_default
t.rename
t.references
t.belongs_to
t.string
t.text
t.integer
t.float
t.decimal
t.datetime
t.timestamp
t.time
t.date
t.binary
t.boolean
t.remove
t.remove_references
t.remove_belongs_to
t.remove_index
t.remove_timestamps
end
http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html
Nick Davies answer is the most complete in terms of adding timestamp columns to a table with existing data. Its only downside is that it will raise ActiveRecord::IrreversibleMigration on a db:rollback.
It should be modified like so to work in both directions:
def change
add_timestamps :campaigns, default: DateTime.now
change_column_default :campaigns, :created_at, from: DateTime.now, to: nil
change_column_default :campaigns, :updated_at, from: DateTime.now, to: nil
end
The issue with most of the answers here is that if you default to Time.zone.now all records will have the time that the migration was run as their default time, which is probably not what you want. In rails 5 you can instead use now(). This will set the timestamps for existing records as the time the migration was run, and as the start time of the commit transaction for newly inserted records.
class AddTimestampsToUsers < ActiveRecord::Migration
def change
add_timestamps :users, default: -> { 'now()' }, null: false
end
end
def change
add_timestamps :table_name
end
Using Time.current is a good style https://github.com/rubocop-hq/rails-style-guide#timenow
def change
change_table :users do |t|
t.timestamps default: Time.current
t.change_default :created_at, from: Time.current, to: nil
t.change_default :updated_at, from: Time.current, to: nil
end
end
or
def change
add_timestamps :users, default: Time.current
change_column_default :users, :created_at, from: Time.current, to: nil
change_column_default :users, :updated_at, from: Time.current, to: nil
end
I'm on rails 5.0 and none of these options worked.
The only thing that worked was using the type to be :timestamp and not :datetime
def change
add_column :users, :created_at, :timestamp
add_column :users, :updated_at, :timestamp
end
not sure when exactly this was introduced, but in rails 5.2.1 you can do this:
class AddTimestampsToMyTable < ActiveRecord::Migration[5.2]
def change
add_timestamps :my_table
end
end
for more see "using the change method" in the active record migrations docs.
This seems like a clean solution in Rails 5.0.7 (discovered the change_column_null method):
def change
add_timestamps :candidate_offices, default: nil, null: true
change_column_null(:candidate_offices, :created_at, false, Time.zone.now)
change_column_null(:candidate_offices, :created_at, false, Time.zone.now)
end
A lot of answers here, but I'll post mine too because none of the previous ones really worked for me :)
As some have noted, #add_timestamps unfortunately adds the null: false restriction, which will cause old rows to be invalid because they don't have these values populated. Most answers here suggest that we set some default value (Time.zone.now), but I wouldn't like to do that because these default timestamps for old data will not be correct. I don't see the value in adding incorrect data to the table.
So my migration was simply:
class AddTimestampsToUser < ActiveRecord::Migration
def change_table
add_column :projects, :created_at, :datetime
add_column :projects, :updated_at, :datetime
end
end
No null: false, no other restrictions. Old rows will continue being valid with created_at as NULL, and update_at as NULL (until some update is performed to the row). New rows will have created_at and updated_at populated as expected.
I made a simple function that you can call to add to each table (assuming you have a existing database) the created_at and updated_at fields:
# add created_at and updated_at to each table found.
def add_datetime
tables = ActiveRecord::Base.connection.tables
tables.each do |t|
ActiveRecord::Base.connection.add_timestamps t
end
end
add_timestamps(table_name, options = {}) public
Adds timestamps (created_at and updated_at) columns to table_name. Additional options (like null: false) are forwarded to #add_column.
class AddTimestampsToUsers < ActiveRecord::Migration
def change
add_timestamps(:users, null: false)
end
end
This is a simple one to add timestamp in existing table.
class AddTimeStampToCustomFieldMeatadata < ActiveRecord::Migration
def change
add_timestamps :custom_field_metadata
end
end
In rails 6 (and possibly earlier) if you try to add timestamps to an existing table with records already present like this:
def change
add_timestamps :table_name
end
you will get an error owing to the fact that add_timestamps by default declares the new colums as NOT NULL. You can work around this simply by adding null: true as an argument:
def change
add_timestamps :table_name, null: true
end
The answers before seem right however I faced issues if my table already has entries.
I would get 'ERROR: column created_at contains null values'.
To fix, I used:
def up
add_column :projects, :created_at, :datetime, default: nil, null: false
add_column :projects, :updated_at, :datetime, default: nil, null: false
end
I then used the gem migration_data to add the time for current projects on the migration such as:
def data
Project.update_all created_at: Time.now
end
Then all projects created after this migration will be correctly updated. Make sure the server is restarted too so that Rails ActiveRecord starts tracking the timestamps on the record.
You can use a migration like this to add a created_at and updated_at columns to an existing table with existing records. This migration sets the created_at and updated_at fields of existing records to the current date time.
For the sake of this example say the table name is users and the model name is User
class AddTimestampsToTcmOrders < ActiveRecord::Migration[6.0]
def up
# Add timestamps to the users table with null as true cause there are existing records
add_timestamps(:users, null: true)
# Update existing records with non-nil timestamp values
User.update_all(created_at: DateTime.now, updated_at: DateTime.now)
# change columns so they can't be nil
change_column(:users, :updated_at, :datetime, null: false, precision: 6)
change_column(:users, :created_at, :datetime, null: false, precision: 6)
end
def down
remove_column :users, :updated_at
remove_column :users, :created_at
end
end
For those who don't use Rails but do use activerecord, the following also adds a column to an existing model, example is for an integer field.
ActiveRecord::Schema.define do
change_table 'MYTABLE' do |table|
add_column(:mytable, :my_field_name, :integer)
end
end
It's change, not change_table for Rails 4.2:
class AddTimestampsToUsers < ActiveRecord::Migration
def change
add_timestamps(:users)
end
end
I personally used the following, and it updated all previous records with the current time/date:
add_column :<table>, :created_at, :datetime, default: Time.zone.now, null: false
add_column :<table>, :updated_at, :datetime, default: Time.zone.now, null: false
I ran into the same issue on Rails 5 trying to use
change_table :my_table do |t|
t.timestamps
end
I was able to add the timestamp columns manually with the following:
change_table :my_table do |t|
t.datetime :created_at, null: false, default: DateTime.now
t.datetime :updated_at, null: false, default: DateTime.now
end

Resources