Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 5.2 Error Changing or Removing Table Column (SQLite3::ConstraintException: FOREIGN KEY constraint failed: DROP TABLE)

I'm trying to accomplish the fairly simple feat of changing the default value for one of the columns from my Blog table. I have the following migration:

class UpdateBlogFields < ActiveRecord::Migration[5.2]
  def change
    change_column :blogs, :freebie_type, :string, default: "None"
  end
end

Fairly simple, but I'm getting the following error when I run rake db:migrate:

StandardError: An error has occurred, this and all later migrations canceled:
SQLite3::ConstraintException: FOREIGN KEY constraint failed: DROP TABLE "blogs"

I get this error any time I try to change or remove a column, but not when adding one.

My schema looks like this:

  create_table "blogs", force: :cascade do |t|
    t.string "title"
    t.string "teaser"
    t.text "body"
    t.string "category", default: "General"
    t.string "linked_module"
    t.boolean "published", default: false
    t.datetime "published_on"
    t.integer "user_id"
    t.integer "image_id"
    t.integer "pdf_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "slug"
    t.string "cta_read_more", default: "Read More"
    t.string "cta_pdf", default: "Get My Free PDF"
    t.string "cta_video", default: "Watch the Video"
    t.string "convertkit_data_form_toggle"
    t.string "convertkit_href"
    t.integer "pin_image_id"
    t.string "data_pin_description"
    t.string "freebie_filename"
    t.string "video_link"
    t.string "freebie_type", default: "File"
    t.string "freebie_description"
    t.integer "comments_count"
    t.integer "subcategory_id"
    t.boolean "affiliate_links", default: true
    t.boolean "approved", default: false
    t.boolean "submitted", default: false
    t.index ["image_id"], name: "index_blogs_on_image_id"
    t.index ["pdf_id"], name: "index_blogs_on_pdf_id"
    t.index ["pin_image_id"], name: "index_blogs_on_pin_image_id"
    t.index ["slug"], name: "index_blogs_on_slug", unique: true
    t.index ["subcategory_id"], name: "index_blogs_on_subcategory_id"
    t.index ["user_id"], name: "index_blogs_on_user_id"
  end

It seems that this might be an SQLite thing, because this post and this one seem to be having a similar problem. However, neither post involves an actual answer. Has anyone successfully gotten rid of this?

like image 574
Liz Avatar asked Dec 02 '18 16:12

Liz


People also ask

How do I fix foreign key constraint failure?

The error message itself showing there is a foreign key constraint error, which means you are deleting a parent table where the child table contains the Primary table identifier as a foreign key. To avoid this error, you need to delete child table records first and after that the parent table record.

How do I drop a foreign key constraint in SQLite?

How to Drop a Foreign Key on a Table. You can not use the ALTER TABLE statement to drop a foreign key in SQLite. Instead you will need to rename the table, create a new table without the foreign key, and then copy the data into the new table.


2 Answers

UPDATE:

A new column default can be added via Rails without having to use the database. In the Blog model, we can use ActiveRecord::Attributes::ClassMethods::attribute to redefine the default value for freebie_type:

attribute :freebie_type, :string, default: 'None'

This will change the default at the business logic level. Therefore, it is dependent on using ActiveRecord to be recognized. Manipulation of the database via SQL will still use the old default. To update the default in all cases see the original answer below.

ORIGINAL ANSWER:

Unfortunately, ALTER COLUMN is only minimally supported by SQLite. The work around it to create a new table, copy the information to it, drop the old table, and finally rename the new table. This is what Rails is attempting to do, but without first disabling the foreign key constraints. The foreign key relations to user_id, image_id, and pdf_id are preventing the table deletion.

You will need to do the update manually, either with SQL (preferred) or ActiveRecord::Base.connection. You can see the process here under 'Modify column in table'. You can find all the options available for columns in the SQLite Create Table Documentation.

PRAGMA foreign_keys=off;

BEGIN TRANSACTION;

ALTER TABLE table1 RENAME TO _table1_old;

CREATE TABLE table1 (
( column1 datatype [ NULL | NOT NULL ] DEFAULT (<MY_VALUE>),
  column2 datatype [ NULL | NOT NULL ] DEFAULT (<MY_VALUE>),
  ...
);

INSERT INTO table1 (column1, column2, ... column_n)
  SELECT column1, column2, ... column_n
  FROM _table1_old;

COMMIT;

PRAGMA foreign_keys=on;

Be certain that you have all the columns set up the way you want as you will not be able to fix it after the table is created! Going forward I would highly recommend setting up either a PostgreSQL or MySQL2 database. They are much more powerful and will be much easier to modify and maintain.

like image 95
Tom Avatar answered Sep 28 '22 10:09

Tom


You can add an initializer to monkey patch the sqlite adapter to make it work with rails 5, just make sure you have sqlite >= 3.8, with this code:

blog/config/initializers/sqlite3_disable_referential_to_rails_5.rb

Content:

require 'active_record/connection_adapters/sqlite3_adapter'

module ActiveRecord
  module ConnectionAdapters
    class SQLite3Adapter < AbstractAdapter

      # REFERENTIAL INTEGRITY ====================================

      def disable_referential_integrity # :nodoc:
        old_foreign_keys = query_value("PRAGMA foreign_keys")
        old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys")

        begin
          execute("PRAGMA defer_foreign_keys = ON")
          execute("PRAGMA foreign_keys = OFF")
          yield
        ensure
          execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}")
          execute("PRAGMA foreign_keys = #{old_foreign_keys}")
        end
      end

      def insert_fixtures_set(fixture_set, tables_to_delete = [])
        disable_referential_integrity do
          transaction(requires_new: true) do
            tables_to_delete.each {|table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete"}

            fixture_set.each do |table_name, rows|
              rows.each {|row| insert_fixture(row, table_name)}
            end
          end
        end
      end

      private

      def alter_table(table_name, options = {})
        altered_table_name = "a#{table_name}"
        caller = lambda {|definition| yield definition if block_given?}

        transaction do
          disable_referential_integrity do
            move_table(table_name, altered_table_name,
                       options.merge(temporary: true))
            move_table(altered_table_name, table_name, &caller)
          end
        end
      end
    end
  end
end

Here is the gist: https://gist.github.com/javier-menendez/3cfa71452229f8125865a3247fa03d51

like image 41
Javier Menéndez Rizo Avatar answered Sep 28 '22 10:09

Javier Menéndez Rizo