Une application Rails en production doit pouvoir être déployée sans downtime. Ajouter des index en BDD sans verrouiller les tables en fait partie — mais la façon dont Rails gère ce mécanisme cache quelques subtilités qui peuvent laisser votre base de données dans un état incohérent et forcer une intervention manuelle au pire moment.

Le problème avec la création d'index bloquante

Lorsque vous intégrez strong_migrations dans un projet Rails, l'un des premiers avertissements que vous rencontrerez est celui-ci :

Adding an index non-concurrently blocks writes.

Sur une table volumineuse, un add_index standard peut bloquer les écritures pendant plusieurs secondes — voire bien plus si la table est suffisemment grande. En production, cela signifie une interruption de service. La solution est simple : utiliser CREATE INDEX CONCURRENTLY de PostgreSQL, exposé dans Rails via algorithm: :concurrently.

class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    add_index :users, :email, algorithm: :concurrently
  end
end

L'appel à disable_ddl_transaction! est obligatoire ici. Par défaut, Rails encapsule chaque migration dans une transaction — un filet de sécurité : si quelque chose tourne mal en cours de migration, tout est annulé automatiquement. Mais PostgreSQL interdit explicitement d'exécuter CREATE INDEX CONCURRENTLY dans un bloc de transaction. On désactive donc ce mécanisme.

C'est un pattern bien connu. Ce dont on parle moins, c'est ce qui se passe quand les choses tournent mal à l'intérieur d'une migration qui a désactivé les transactions.

Le danger caché : les échecs partiels sans rollback

Imaginez que vous ayez plusieurs opérations à effectuer sur la table users :

class AddIndicesToUsersEmailAndPhoneNumber < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    add_index :users, :email, algorithm: :concurrently
    raise "something goes wrong here"
    add_column :users, :phone_number
  end
end

La migration échoue sur la deuxième instruction. Jusque-là, rien d'inattendu. Vous corrigez le problème et relancez db:migrate.

Cette fois, vous obtenez une erreur complètement différente :

PG::DuplicateTable: ERROR: relation "index_users_on_email" already exists

En l'absence de transaction, le premier add_index a été validé immédiatement lors de son exécution. Quand la migration a échoué, il n'y avait rien à annuler. L'index existe désormais dans la base de données, mais Rails considère toujours la migration comme en attente — donc au prochain lancement, il tente de créer le même index, et PostgreSQL refuse.

La seule solution est de supprimer manuellement l'index orphelin directement sur la base de données. Sur un environnement de staging, c'est une contrainte. En production, c'est un risque opérationnel sérieux.

La solution : une seule instruction par migration sans transaction

La règle est simple : chaque migration utilisant disable_ddl_transaction! ne doit contenir qu'une seule instruction. Si vous faites deux opérations index, écrivez deux migrations.

# Migration 1
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    add_index :users, :email, algorithm: :concurrently
  end
end

# Migration 2
class AddPhoneNumberToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :phone_number
  end
end

Cela garantit que tout échec est totalement isolé. Chaque migration est soit complètement appliquée, soit pas du tout — ce qui vous restitue la garantie de sécurité qu'offrent normalement les transactions.

Automatiser la vérification avec un cop RuboCop personnalisé

Les best practice en code review c'est bien, une règle automatisée c'est mieux. Voici un RuboCop custom qui signale toute migration disable_ddl_transaction! dont la méthode change, up ou down contient plus d'une instruction. L'implémentation complète est disponible sous forme de GitHub Gist.

Déposez-le dans votre répertoire custom_cops/, configurez-le dans .rubocop.yml, et votre CI bloquera les violations avant qu'elles n'atteignent la production.

Ce qu'il faut retenir

disable_ddl_transaction! n'est pas une simple formalité PostgreSQL — cela change fondamentalement la sémantique d'échec de votre migration. Dès que vous désactivez les transactions, vous perdez le rollback automatique, et chaque instruction devient permanente dès son exécution. Limitez ces migrations à une seule instruction, automatisez la vérification, et vous n'aurez plus jamais à supprimer manuellement des index orphelins sur une base de données de production.