Cannot add a NOT NULL column with default value NULL in Sqlite3 - ruby-on-rails

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.

Related

Remove _id suffix from foreign key migration

I am trying to add a foreign key to via migration. It works as expected, but it automatically adds _id to the end (the column name I want to reference doesn't include _id). How can I make it reference the column name as I give it?
Here is the migration
class ChangeRefOnMemberPresentations < ActiveRecord::Migration[5.2]
def change
add_reference :member_presentations, 'employee_number', foreign_key: { to_table: :users }
end
end
Which results in both the reference column name and foreign key reference column being called employee_number_id in schema.rb
The following worked by defining everything manually, but seems messy. If there is a better migration answer, I'll be happy to accept.
def change
# column was added in another migration, but including for completness
add_column :member_presentations, :employee_number, :bigint
add_index :member_presentations, :employee_number, name: "index_member_presentations_on_employee_number"
add_foreign_key :member_presentations, :users, column: "employee_number"
end

How to change already added string column to a Reference in Rails

I've already created a model in Rails to collect some user information
I created the columns as :string initially but I've since changed the way this data is looked up and entered by using separate populated models.
Now instead of entering into these fields as string - i want these columns to be "references" instead.
Is there an easy way to change from the string to reference without having to create a new model entirely?
*do not need to save the existing data
Is there any data in the strings you would like to save?
Or is it just because it has the same name?
You don't have to create a new model.
You could create a simple migration
remove_column :table, :your_column_name, :string
add_column :table, :your_column_name, :integer, references: :your_parent_model
You can add a temporary string column to save the string column first:
rails g migration add_temporary_string_column_to_model temporary_string_column:string
And run rails console:
SomeModel.all.each do |some_model|
some_mode.temporary_string_column = some_mode.string_column
some_mode.save
end
And now you can change your original string column's type to references which is an int(4) column in MySQL, migration like this:
class ChangeFormatInSomeTable < ActiveRecord::Migration
def change
change_column :some_table, :string_column, :references
end
end
Finally, you can run rails console again to convert the string data to integer like this:
SomeModel.all.each do |some_model|
some_mode.string_column = some_mode.temporary_string_column.to_i
some_mode.save
end
And at last, remove the temporary string column:
rails g migration remove_temporary_string_column_from_model temporary_string_column
Here is another solution, without dropping the column itself (not exactly in my case). I'm not sure though if this is the best solution.
In my case, I have a tickets table that holds purchase_uid in itself. I decided to keep purchases in another table after making the necessary improvements in our backend. Purchases table has uuid as the primary key. Given this background, here is my migration to change my column into a reference.
class AddPurchaseRelationToTickets < ActiveRecord::Migration[5.2]
def up
change_column :tickets, :purchase_uid, :uuid, references: :purchase, foreign_key: true, using: 'purchase_uid::uuid'
end
def down
change_column :tickets, :purchase_uid, :string
end
end
In my case, since string doesn't automatically cast into uuid, purchase_uid were dropped and recreated as well. However, if you decide to keep the column type same, I don't think it will be a problem.
You can create migrations to serve the exact purpose.
rails generate migration AddAddressToUsers address:references
This will create a migration file in db/migrate directory.
Then run: rails db:migrate to run migration and make changes in your database.
Don't forget to create associations in your models (belongs_to, has_many, etc.) depending on your system structure.
Wanted to add a simpler alternative to the accepted answer that preserves data:
class ChangeStringToInt < ActiveRecord::Migration[5.1]
def up
change_column :table_name, :field_name, :integer, null: false, references: :table_referenced, using: 'field_name::integer'
add_index :chapter_actions, :field_name
end
def down
change_column :table_name, :field_name, :string, null: false, using: 'field_name::character varying'
remove_index :table_name, :field_name
end
end

Re-create primary key in Rails

I previously converted one of my tables' id column to bigint, and populated it with bigint values. I'm now regretting that as I can't seem to update values via ActiveRecord without runnning into RangeErrors.
I'd like to convert the id column back to the standard integer type, and clear out the existing values and populate it with auto-incremented integer values.
I don't have any foreign keys / relations I need to worry about.
Is there a straightforward way to do this via a Rails migration (Rails 4 / Postgres)?
The prior migration:
class ConvertWidgetPkToBigint < ActiveRecord::Migration
def change
change_column :widgets, :id, :bigint
end
end
Current schema.rb:
create_table "widgets", id: :bigserial, force: :cascade do |t|
...
end
Figured it out:
class RecreatePrimaryKey < ActiveRecord::Migration
def change
execute "UPDATE widgets SET id = DEFAULT"
change_column :widgets, :id, :integer
end
end
Basically it re-populates the column with auto-increment values, and then converts the column back to type integer.

reversible and revert in Active Record migrations

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.

RAILS: DB MIGRATION: resetting id column to auto_increment

When I created the table users, the resulting table had a column called id, defined as an integer.
I tried to modify it to bigint, unsigned as follows:
change_column :users, :id, :integer, :limit => 8, :unsigned => true
Which did change it to bigint, but it was no longer an auto-increment column (though it was still identified as the primary index, and it was not set to unsigned (even though rails told me that the migration executed fine)
I then tried doing:
change_column :users, :id, :integer, :limit => 8, :unsigned => true, :null => false, :auto_increment => true
Rails said that the migration executed fine, but nothing changed.
I could try something like:
change_column :users, :id, :primary_index
but that would put me right where I started
I could also try an "execute" statement with MySQL code, but I want to keep the migration file "clean". Has anyone run into this issue?
As an aside, I was also trying to set the default to NULL on another column, researched it here (and Google), with no success.
EDIT:
It seems as if there is no way to edit the column "id" after it is created as part of table creation through a generic migration. The only way to do this is through an "execute" statement with MySQL syntax.
Try creating and running this migration:
class ChangeColumnUserIdToAutoIncrement < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users modify COLUMN id int(8) AUTO_INCREMENT"
end
def self.down
execute "ALTER TABLE users modify COLUMN id int(8)"
end
end
Rails is taking care for you the ID field of all the tables you're creating and it's hidden from your migrations and even from the schema.rb file.
So I would advice you to take a step back and think: Why would you want to mess with it? Are you sure that's really needed?
EDIT: This answer seems to be exactly what you're looking for.
Regarding "setting default to NULL", again, why would you do that? All the columns are defaulting to NULL anyway. However, if that's not the case you could do that in a migration:
change_table :users do |t|
t.string :description, :default => nil
end
Hope that answers your question.

Resources