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.
Related
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
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
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
I have ran my migrations on my production server and I am using MySQL, I get this error:
Mysql2::Error: Invalid default value for 'admin': ALTER TABLE users ADD admin tinyint(1) DEFAULT 'false'`
my migration looks like this:
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: :false
end
end
I understand the error is because "false" is not a proper value for a tinyint, this should be a 0 in this case. I thought default: :false was the right way to default a boolean to false.
How do I fix this so MySQL does not complain about the bad value?
false is not a symbol I believe. Try this
add_column :users, :admin, :boolean, default: false
PS
I am wrong. So you should set default: 0 :(. Or you can patch ActiveRecord::Migration so it will accept true|false
This works in both PostgreSQL and MySQL:
add_column :users, :admin, :boolean, :default => false
I haven't tried this with Ruby 1.9.2's new hash syntax, but I don't think that will be an issue.
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