Add NOT NULL field without default value into a populated DB - ruby-on-rails

I have a table, let's call it MyTable. It is part of a Postgresql database.
In MyTable are a lot of entries, let's say over a million. I would like to add a field to this table, let's call it MyNewField. It is to be added by an ActiveRecord Migration.
This field is to be without default values and not nullable. The result, in it's migration class would be something like so:
class AddMyFieldToMyTable < ActiveRecord::Migration
def change
add_column :my_table, :my_field, :text, null: false
end
end
However, it will trigger an error (PG::NotNullViolation), because the table already contains rows, all which will have MyField set to NULL.
What I would like to do is: Add the row without default value and nullable set to false (without triggering a PG::NotNullViolation). Then, insert a value from another table into each records.
This would probably be achievable by adding the field with nullable set to true, then adding the values, then changing back to nullable set to false. However, I am interested to know if it is possible to do so in a single shot.

You have to make sure that the other table has the my_field value for each entry in my_table.
class AddMyFieldToMyTable < ActiveRecord::Migration
def up
add_column :my_table, :my_field, :text
execute("insert into my_table(my_field) values (select my_field from different_table where my_table.id = different_table.different_id)")
change_column :my_table, :my_field, :text, null: false
end
def down
remove_column :my_table, :my_field
end
end

All indications seems to show that this will not be possible to do in one shot; you'll have to add the column without the null constraint, populate the data, then add the null constraint afterwards.
class AddMyFieldToMyTable < ActiveRecord::Migration
def change
add_column :my_table, :my_field, :text
reversible do |dir|
dir.up do
# populate my_field col
change_column :my_table, :my_field, :text, null: false
end
end
end
end
Resources:
How to solve "Cannot add a NOT NULL column with default value NULL" in SQLite3?
http://strd6.com/2009/04/adding-a-non-null-column-with-no-default-value-in-a-rails-migration/
If you really only want to set the column once, perhaps you could generate it with a temporary default value then immediately update it with the real data.
class AddMyFieldToMyTable < ActiveRecord::Migration
def change
add_column :my_table, :my_field, :text, default: 'tmp', null: false
reversible do |dir|
dir.up do
# populate my_field col
end
end
end
end

In my case I need to deviate from #usha's answer in two ways: 1) I needed to UPDATE rather than INSERT INTO since I'm updating the NULL values of existing rows rather than creating new rows. 2) I needed to use change_column_null instead of change_column ... null: false. A stylized example is below.
class AddMyFieldToMyTable < ActiveRecord::Migration
def up
add_column :my_table, :my_field, :text
execute <<-SQL
WITH values AS (
SELECT
my_table.id AS my_table_id,
different_table.my_value
FROM my_table
JOIN different_table ON
my_table.id = different_table.different_id
)
UPDATE my_table
SET my_field = values.my_value
FROM values
WHERE my_table.id = values.my_table_id
SQL
change_column_null :my_table, :my_field, false
end
def down
remove_column :my_table, :my_field
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 migration add default not null properties to existing boolean field

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.

Is there any way to rearrange the table's fields in Rails migrations?

After the table creation, I want to rearrange its fields in a different order, and take care about its stored data.
Is there any way to do this?
You can use after in a change column statement:
def up
change_column :your_table, :some_column, :integer, after: :other_column
end
You have to create a separate migration to change column.
rails g migration change_data_type_for_fieldname
class ChangeDataTypeForFieldname < ActiveRecord::Migration
def up
#change column type form int to bigint
change_column :my_table, :my_column, :bigint
end
def down
#change column type form bigint to int
change_column :my_table, :my_column, :int
end
end
Please refer api

Rails: change column type, but keep data

I have a model with a column of type integer which I want to convert to type string. Now I'm looking for the best way to change the column type without losing the data. Is there a painless way to accomplish this?
A standard migration using the change_column method will convert integers to strings without any data loss. rake db:rollback will also do the reverse migration without error if required.
Here is the test migration I used to confirm this behaviour:
class ChangeAgeToString < ActiveRecord::Migration
def self.up
change_column :users, :age, :string
end
def self.down
change_column :users, :age, :integer
end
end
for postgres
in migration
change_column :table_name, :field,'boolean USING (CASE field WHEN \'your any string as true\' THEN \'t\'::boolean ELSE \'f\'::boolean END)'
and to any valid type similar
for postgresql, change table column datatype integer to string,
rails migration like this with up and down actions
class ChangeAgeToString < ActiveRecord::Migration
def self.up
change_column :users, :age, 'varchar USING CAST(age AS varchar)', null: false
end
def self.down
change_column :users, :age, 'integer USING CAST(age AS integer)', null: false, default: 0
end
end
If it's a one-off you can just change the column type in the database (since no info is lost moving from int to varchar)
For MySQL, this would do it:
ALTER TABLE t1 MODIFY col1 VARCHAR(256)
If you're using SQLite you won't have to do anything.
You can try something like this:
change_column :table_name, :column_name, 'integer USING CAST(column_name AS integer)'
or even better:
change_column :table_name, :column_name, :integer, using: 'column_name::integer'
You can read more about this topic here: https://kolosek.com/rails-change-database-column
If you use Postgres, you can't implicitly cast a string back to an integer, so the way to make the change reversible is:
class ChangeAgeToString < ActiveRecord::Migration
def self.up
change_column :users, :age, :string
end
def self.down
add_column :age_integer
User.connection.execute('UPDATE users SET age_integer = cast(age as int)')
remove_column :users, :age
rename_column :users, :age_integer, :age
end
end

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