`add_index`の配列構文で式インデックスが使えるように

rails/rails

add_indexメソッドで配列構文を使った式インデックスが正しく機能しない問題が修正されました。これにより、["lower(email)", :status]のような配列形式で式インデックスを定義できるようになります。

背景

add_indexでは従来から式インデックスをサポートしていましたが、文字列形式のみが機能する状態でした。t.index "lower(email)"は動作する一方、t.index ["lower(email)"]はSQLiteであればSQLite3::SQLException: no such column: "lower(email)"というエラーを引き起こしていました。

問題の根本は、配列構文で渡された際の処理にありました。文字列をそのままSQL文に渡す場合は式として解釈されますが、配列として渡すと各要素がカラム名として扱われ、lower(email)がカラム名として検索されてしまいます。#55099でこのバグが報告されています。

配列構文は複数カラムのインデックスでは一般的な記法であり、式インデックスでも同じ書き方ができないことは一貫性を欠いていました。

技術的な変更

index_column_namesメソッドのロジックが変更され、配列内に式が含まれる場合を適切に検出して処理するようになりました。

変更の対象はactiverecord/lib/active_record/connection_adapters/abstract/schema_statements.rb内のindex_column_namesメソッドです。

変更前:

def index_column_names(column_names)
  if expression_column_name?(column_names)
    column_names
  else
    Array(column_names)
  end
end

変更後:

def index_column_names(column_names)
  column_names = Array(column_names)
  return column_names.join(", ") if has_expression_column_name?(column_names)

  column_names
end

def has_expression_column_name?(column_names)
  column_names.any? { |name| expression_column_name?(name) }
end

変更前はexpression_column_name?を配列全体に対して評価していましたが、変更後はまず入力をArray()で統一したうえで、新たに追加したhas_expression_column_name?メソッドによって各要素を個別にチェックします。配列内にひとつでも式が含まれていれば、join(", ")でカンマ区切りの文字列に変換してSQLに渡す形式に統一されます。

これにより、以下のいずれの書き方も正しく動作します:

  • add_index :users, "lower(email)"(従来の文字列形式、変更なし)
  • add_index :users, ["lower(email)"](配列に式が1つ)
  • add_index :users, ["lower(email)", :status](式とカラム名の混在)
  • add_index :users, ["(lower(last_name))", "(upper(first_name))"](複数の式)

設計判断

配列内に式が1つでも含まれていれば、配列全体をjoin(", ")で文字列化してSQLに渡す設計が採用されました。

このアプローチは、式とカラム名が混在した["lower(email)", :status]のようなケースを扱うための自然な拡張です。既存の文字列形式の式インデックス処理(column_namesをそのまま返す)と同じSQL表現に帰着させることで、アダプター側の処理を変更せずに済んでいます。

has_expression_column_name?を独立したメソッドとして切り出したことで、責務の分離と可読性の向上が図られています。テストではsupports_expression_index?によってアダプターのサポート状況を確認したうえで、式単体・式+カラム名・複数式の各パターンが網羅されています。

まとめ

本PRは、配列構文の式インデックスが暗黙的にカラム名として解釈されていた問題を、index_column_namesの入力正規化と要素単位の式検出によって修正しました。これにより、複数カラムインデックスで馴染みのある配列構文が式インデックスでも一貫して使えるようになり、マイグレーションコードの可読性と記述の統一性が向上します。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
30b951be

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が明確です。リード文、背景、技術的な変更、設計判断、まとめがそれぞれ適切に配置されており、読者が変更の全体像をスムーズに理解できる構成になっています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)や、PR・Issueへのリンク記法([#123](URL))がガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容はActive Recordのマイグレーションに関するもので、専門知識を持つエンジニアを対象としており、過度な説明がなく適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論で構成され、段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより、記事の論理構造が非常に明快になっています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている変更前後のコードは、提供されたDiff情報と完全に一致しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「式インデックス」「配列構文」「アダプター」など、文脈に合った正確な技術用語が使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コード変更の技術的な解説は正確です。`has_expression_column_name?`メソッドの導入意図や、`join(", ")`による文字列化の挙動など、変更内容が論理的に正しく説明されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのTitle, Description, Diff、または関連するIssue(#55099)の内容によって裏付けられています。「設計判断」のセクションもコードの変更から論理的に導出できる範囲であり、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#55100)およびIssue番号(#55099)は正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「`add_index`の配列構文で式インデックスが使えるように」は、PRの主題を的確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPRで提供された情報に限定されており、バージョンのサポート状況やリリース予定といった、PRに記載のない外部知識の追記はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「従来は〜だったが、今回の変更で〜できるようになった」という時間的な前後関係の表現は、PRの内容と一致しており正確です。