1

In our database we currently have a table with a String country column. This stores a single country code (e.g. US). The country column has an index. We now need to store multiple country codes in the column, so we want to convert it to a PostgreSQL String array. My current migration code is

  def change
    reversible do |direction|
      change_table :product do |table|
        direction.up do
          table.remove_index(:country)
          table.rename :country, :countries
          table.change :countries, :string, array: true
          table.index :countries
        end

        direction.down do
          table.remove_index(:countries)
          table.change :countries, :string, array: false
          table.rename :countries, :country
          table.index :country
        end
      end
    end
  end

however when I run the migration I get the error

PG::DatatypeMismatch: ERROR:  column "countries" cannot be cast automatically to type character varying[]
HINT:  You might need to specify "USING countries::character varying[]"

and I am not sure how to specify how I want the conversion to be performed.

I would like to know how to change my migration so that

  1. The countries column is an array
  2. The countries column is indexed
  3. The existing string value of the country column is saved into the array

or in other words, so that

country: 'US'

becomes

countries: ['US']
1
  • I'm not aware of any way to do this in a Rails migration without using SQL, which the error message generously assisted with. How is the array to be meaningfully indexed? Are there any constraints on the country values? Instead of changing the column to an array, consider creating a new table, product_countries, with two columns, product_id and country, with a foreign key from product_id referencing the product table. Commented Apr 5, 2023 at 16:38

1 Answer 1

1

This is not a safe way to do this kind of migration. You should instead:

  1. Create a migration to create a new column on the table:
class AddCountriesToProduct < ActiveRecord::Migration
  def change
    add_column :countries, :product, :string, array: true, default: []
  end
end
  1. Update your model to write to both columns concurrently, for example in an after_save callback so that changes to country are propagated to countries. At this point the two columns will not be completely in sync.
  2. Save every product record (e.g., Product.find_each(&:save!)) so that the callback is triggered, pushing the values from country to countries. At this point the two columns will be completely in sync.
  3. Update your app so that it no longer uses the country attribute and uses only the countries attribute. After this the two columns will no longer be in sync but countries will have the correct values.
  4. Create a migration to drop the old column from the table:
class RemoveCountryFromProduct < ActiveRecord::Migration
  def change
    remove_column :country, :product
  end
end

After you complete these steps you can begin using the countries column to store multiple strings.

A couple of notes:

  • It's possible to do everything in a single migration if you don't mind locking the table and can handle downtime but for safe online migrations you don't want to do this.
  • Your table is named product and not products which breaks Rails conventions. I've respected your table name in this example but you should consider fixing this.
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.