MySQLのDDL操作に`lock:`オプションと`algorithm:`の適用範囲を拡張
MySQLのDDL操作でalgorithm:とlock:オプションをカラム操作(add_column、change_column、remove_column、rename_column)にも指定できるようになりました。これにより、大規模なプロダクションテーブルへのオンラインスキーマ変更をマイグレーションDSLで記述できます。
背景
RailsはすでにMySQLのインデックス操作にalgorithm:オプション(:default、:copy、:inplace、:instant)をサポートし、PostgreSQLではalgorithm: :concurrentlyをサポートしていました。しかし、カラム操作(add_column、change_column、remove_column、rename_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_optionsとlock_clauseの追加
abstract_mysql_adapter.rb にlock_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.rbにadd_columnとchange_columnのオーバーライドが追加され、mysql/schema_statements.rbにremove_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.rbのvisit_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.rb のinvert_rename_columnがoptionsを第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_clauseはlock_options.fetchにフォールバックブロックを渡すことで、無効な値への早期エラーを実現しており、index_algorithmのindex_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:をカラム操作に指定できるようになり、既存の実装パターンを踏襲することで追加の学習コストなくチームへ導入できます。