Josh McArthur: Fancy Postgres indexes with ActiveRecord

Posted . Visible to the public.

I recently wanted to add a model for address information but also wanted to add a unique index to those fields that is case-insensitive.
The model looked like this:

create_table :shop_locations do |t|
  t.string :street
  t.string :house_number
  t.string :zip_code
  t.string :city
  t.belongs_to :shop
end

But how to solve the uniqueness problem?

Another day, another undocumented Rails feature!

This time, it’s that ActiveRecord::Base.connection.add_index supports an undocumented option to pass a string argument as the value for ‘column’.

This string is passed directly to the SQL statement, making it possible to use all sorts of fun things to lock down the constraint. It also means that you can make an index more declaratively if you’re not comfortable using the built-in options for key lengths or ordering.

(excerpt from: Fancy Postgres indexes with ActiveRecord Show archive.org snapshot )

So regarding to the cited site Rails 5+ allows me to use an SQL statement in my index:

t.index 'shop_id, lower(street), lower(house_number), lower(zip_code), lower(city)',
  name: 'index_unique_locations_on_shop_id',
  unique: true

You will also need a validation in your model. Since this is not possible with the existing Rails helpers, you have to write a custom validation like this:

def validate_uniqueness_of_address
  same_address = shop.locations
    .where.not(id: id)
    .where('lower(street) = lower(?)', street)
    .where('lower(house_number) = lower(?)', house_number)
    .where('lower(zip_code) = lower(?)', zip_code)
    .where('lower(city) = lower(?)', city)

  if same_address.exists?
    errors.add(:street, :taken)
    errors.add(:house_number, :taken)
    errors.add(:zip_code, :taken)
    errors.add(:city, :taken)
  end
end

See also:

Florian Leinsinger
Last edit
Florian Leinsinger
License
Source code in this card is licensed under the MIT License.
Posted by Florian Leinsinger to makandra dev (2022-05-18 08:45)