MySQLのDDL操作に`lock:`オプションと`algorithm:`の適用範囲を拡張

rails/rails

MySQLのDDL操作でalgorithm:lock:オプションをカラム操作(add_columnchange_columnremove_columnrename_column)にも指定できるようになりました。これにより、大規模なプロダクションテーブルへのオンラインスキーマ変更をマイグレーションDSLで記述できます。

背景

RailsはすでにMySQLのインデックス操作にalgorithm:オプション(:default:copy:inplace:instant)をサポートし、PostgreSQLではalgorithm: :concurrentlyをサポートしていました。しかし、カラム操作(add_columnchange_columnremove_columnrename_column)ではこれらのオプションが使えませんでした。

MySQLはALGORITHM = {DEFAULT|COPY|INPLACE|INSTANT}LOCK = {DEFAULT|NONE|SHARED|EXCLUSIVE}を使って、DDL操作中のテーブルロック方式を制御できます。しかしRailsのDSLにはこのサポートがなく、大規模テーブルへのオンラインスキーマ変更には生のSQLを書く必要がありました。

# 変更前 — 生SQLを使わざるを得ない
execute "ALTER TABLE users ADD name VARCHAR(255), ALGORITHM = INSTANT, LOCK = NONE"

# 変更後 — マイグレーションDSLで記述できる
add_column :users, :name, :string, algorithm: :instant, lock: :none

この制約を解消したのが本PRです。

技術的な変更

変更は複数のファイルにわたり、lock:オプションの追加とalgorithm:のカラム操作への拡張が行われました。

lock_optionslock_clauseの追加

abstract_mysql_adapter.rblock_optionsメソッドとlock_clauseメソッドが追加されました。lock_optionsは利用可能なロックモードのマッピングを返し、lock_clauseは与えられたキーを検証してSQL句を生成します。

def lock_options
  {
    default:   "LOCK = DEFAULT",
    none:      "LOCK = NONE",
    shared:    "LOCK = SHARED",
    exclusive: "LOCK = EXCLUSIVE",
  }
end

def lock_clause(lock) # :nodoc:
  return unless lock
  lock_options.fetch(lock) do
    raise ArgumentError, "Lock must be one of the following: #{lock_options.keys.map(&:inspect).join(', ')}"
  end
end

無効な値が渡された場合はArgumentErrorを発生させる設計で、既存のindex_algorithmメソッドと同じパターンが踏襲されています。

カラム操作へのalgorithm:lock:の適用

abstract_mysql_adapter.rbadd_columnchange_columnのオーバーライドが追加され、mysql/schema_statements.rbremove_columnのオーバーライドが追加されました。いずれもオプションを事前に取り出し、SQLの末尾に付加するパターンで実装されています。

def add_column(table_name, column_name, type, **options) # :nodoc:
  algorithm = index_algorithm(options.delete(:algorithm))
  lock = lock_clause(options.delete(:lock))
  add_column_def = build_add_column_definition(table_name, column_name, type, **options)
  return unless add_column_def
  sql = schema_creation.accept(add_column_def)
  sql << ", #{algorithm}" if algorithm
  sql << ", #{lock}" if lock
  execute(sql)
end

rename_columnについては、抽象アダプター側のシグネチャをalgorithm:lock:を受け取れるよう変更し、各アダプターの実装がオプションを引き継ぐ形になっています。

インデックス操作へのlock:の追加

CreateIndexDefinition 構造体に:lockフィールドが追加され、mysql/schema_creation.rbvisit_CreateIndexDefinitionがそれを出力するよう拡張されました。

def visit_CreateIndexDefinition(o)
  sql = visit_IndexDefinition(o.index, true)
  sql << " #{o.algorithm}" if o.algorithm
  sql << " #{o.lock}" if o.lock
  sql
end

またvalid_index_options:lockが追加されたことで、MySQL/Trilogyアダプター使用時のみlock:が有効なオプションとして認識されます。

CommandRecorder#invert_rename_columnのオプション保持

command_recorder.rbinvert_rename_columnoptionsを第4引数として受け取り、ロールバック時にもalgorithm:lock:を引き継ぐよう修正されました。

def invert_rename_column(args)
  table_name, old_name, new_name, options = args
  args = [table_name, new_name, old_name]
  args << options if options
  [:rename_column, args]
end

オプションが存在する場合のみ配列に追加する設計で、既存のinvert_rename_columnの動作との後方互換性が維持されています。

設計判断

既存のindex_algorithmと同じパターンを踏襲する設計が採用されました。

PR本文では「PostgreSQLのalgorithm: :concurrentlyサポートおよびMySQLのインデックス操作における既存のalgorithm:サポートと同じパターンで実装した」と明記されています。lock_clauselock_options.fetchにフォールバックブロックを渡すことで、無効な値への早期エラーを実現しており、index_algorithmindex_algorithms.fetchと対称的な設計です。

オプションの抽出はoptions.delete(:algorithm)options.delete(:lock)で行われ、残りのオプションはそのままスーパークラスや内部メソッドに委譲されます。これにより、algorithm:lock:が既存のカラム定義ロジックに干渉しない構造になっています。

valid_index_optionsへの:lock追加はMySQL/Trilogyアダプター限定で行われており、他のアダプターでは無効なオプションとして扱われます。これはMySQLの機能をMySQLアダプター内に閉じ込めるRailsアダプターアーキテクチャの方針と一致しています。

まとめ

本PRは、MySQLのオンラインDDLをRailsマイグレーションDSLで完全に制御できるようにした変更です。生SQLへの逃げ道なしにalgorithm:lock:をカラム操作に指定できるようになり、既存の実装パターンを踏襲することで追加の学習コストなくチームへ導入できます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
3f64404e

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)から始まり、背景、技術的な変更、設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確で、ガイドラインを完全に満たしています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```言語:ファイルパス)とPR番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

DDL、オンラインスキーマ変更、マイグレーションDSLといった用語を前提としており、専門知識を持つエンジニアという対象読者に適合した内容と技術レベルです。

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

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

各セクションが総論から始まり、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすい構造です。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロックは、提供されたDiff情報と完全に一致しており、正確に内容を反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「DDL操作」「algorithm:」「lock:」などの技術用語が、PRの文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

オプションの抽出とSQLへの追記ロジックや、エラーハンドリングの設計など、コード変更に関する技術的な説明が正確で論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのタイトル、Description、Diff内のコードによって裏付けられており、ハルシネーション(創作された情報)は見られません。

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

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

PR番号(#56972)やコード内のキーワード(:instant, :noneなど)が正確に記載されています。

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

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

記事のタイトル「MySQLのDDL操作に`lock:`オプションと`algorithm:`の適用範囲を拡張」は、PRの主題を的確に表現しています。

外部知識の正確性 ✓ PASS

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

記事内容はPR内の情報に限定されており、LTSやリリース日程といったPRに記載のない外部知識の追加はありません。

時間表現の正確性 ✓ PASS

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

PR内の「already supports」といった時間表現を「すでに...サポートしていました」と正しく反映しており、時間的な歪曲はありません。