How to convert an existing text field with Action Text - Ruby on Rails

Recently, I was assigned the task of converting some plain text to rich text in a Rails application. I know it sounds so simple, all you have to do is install Action Text, change the model in question, write a simple migration to convert the plain text to rich text and bob's your uncle, right? Not so fast.

Everything was going swimmingly until I went to write the migration. If you didn't know, Action Text doesn't store the rich text in the same database table as the model. Instead, it creates a polymorphic relationship. Action Text even creates a migration to make a new table when installed. Unfortunately, converting the text is going to be a little more complex than just refactoring our model to have a field with the type "has_rich_text". However, our model does need this so let's go ahead and add it now.

models/post.rb

class Post < ApplicationRecord
  has_rich_text :body
end

Thanks to Felicián Hoppál, and their comment on this issue for a great jumping-off point and simple solution. The only problem is this migration fails when trying to roll back. The reason it fails is the "post.old_body" column doesn't exist. Not to fear, we can overcome this issue by writing separate up and down methods in our migration.

example from github issue

class MigratePostContentToActionText < ActiveRecord::Migration[6.0]
  include ActionView::Helpers::TextHelper
  def change
    rename_column :posts, :body, :old_body
    Post.all.each do |post|
      post.update_attribute(:body, simple_format(post.old_body))
    end
    remove_column :posts, :old_body
  end
end

So let's create our migration and import Text Helper from Action Text.

rails g migration ConvertPostBodyToRichText

Now, we can import Text Helper to help convert plain text to rich text.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
end

Great, so with that out of the way we can start to work on the up method of our migration. We'll start by renaming the column we want to convert to a different name. This allows for the creation/association of the relationship with the Action Text table.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
  def up
    rename_column :posts, :body, :old_body
  end
end

Now, we'll loop through all of our posts and convert them to rich text with the help of the text helper from Action Text.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
  def up
    rename_column :posts, :body, :old_body
    Post.all.each do |post|
      post.update_attribute(:body, simple_format(post.old_body))
    end
  end
end

Finally, we're going to delete the column we renamed since we don't need it anymore.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
  def up
    rename_column :posts, :body, :old_body
    Post.all.each do |post|
      post.update_attribute(:body, simple_format(post.old_body))
    end
    remove_column :posts, :old_body, :text
  end
end

The up method is now finished and we can work on the down method. Remember, the error that occurs on rollback is an undefined method error because the old, renamed column doesn't exist anymore. So the first thing we will do in our down method is create/add that column back.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
  def up
    rename_column :posts, :body, :old_body
    Post.all.each do |post|
      post.update_attribute(:body, simple_format(post.old_body))
    end
  end

  def down
    add_column :posts, :old_body, :text
  end
end

Now that we have created our new column to store our text while we roll back, let's loop through all of our posts and convert our rich text to plain text. We also have to delete the related row in the Action Text table.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
  def up
    rename_column :posts, :body, :old_body
    Post.all.each do |post|
      post.update_attribute(:body, simple_format(post.old_body))
    end
  end

  def down
    add_column :posts, :old_body, :text
    Post.all.each do |post|
      post.update_attribute(:old_body, post.body.to_plain_text)
      post.body.delete
    end
  end
end

Finally, we rename the database column back to its original name.

20211229035004_convert_post_body_to_rich_text.rb

class ConvertPostBodyToRichText < ActiveRecord::Migration[6.1]
  include ActionView::Helpers::TextHelper
  def up
    rename_column :posts, :body, :old_body
    Post.all.each do |post|
      post.update_attribute(:body, simple_format(post.old_body))
    end
  end

  def down
    add_column :posts, :old_body, :text
    Post.all.each do |post|
      post.update_attribute(:old_body, post.body.to_plain_text)
      post.body.delete
    end
    rename_column :posts, :old_body, :body
  end
end

Finally, we can run the migration and safely roll back if needed, with confidence to deploy to production. You should always create a database backup before running a new migration to be safe. 😊

27