Mysql2Adapter が affected_rows を正しく取得できるようにした変更
Mysql2Adapter が affected_rows の取得に失敗しやすいケースを解消し、結果が nil のときでも行数情報を保持できるようにしました。これにより、テストの不安定さが改善され、SQL 実行結果の型が統一されます。
背景
Mysql2Adapter の perform_query が affected_rows を取得できないことが散発的に発生していました。mysql2 0.5.6 以降では、#affected_rows が Mysql2::Error を投げるケースがあり、特に同一接続で準備済みステートメントが GC された場合に顕在化します。この問題は AR テストスイートで再現し、削除スレッドの返り値が期待とずれる原因となっていました。
技術的な変更
perform_query のローカル変数化と結果ラップ
activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rb の perform_query で インスタンス変数 @affected_rows_before_warnings を廃止し、ローカル変数 affected_rows に置き換えました。これによりスレッド間での状態共有がなくなり、安全に行数を保持できます。
@@
- @affected_rows_before_warnings = result&.size || raw_connection.affected_rows
+ affected_rows = result&.size || raw_connection.affected_rows
@@
- @affected_rows_before_warnings = stmt.affected_rows
+ affected_rows = stmt.affected_rows
@@
- intent.notification_payload[:affected_rows] = @affected_rows_before_warnings
+ intent.notification_payload[:affected_rows] = affected_rows
さらに、result が nil の場合は ActiveRecord::Result.empty(affected_rows: affected_rows) を返す ことで、行数情報だけを持つ結果オブジェクトを生成します。result.fields.empty? の際も同様にラップし、未使用の生データは free_raw_result 後に破棄しています。
@@
- if result.nil?
- result = ActiveRecord::Result.empty(affected_rows: affected_rows)
- elsif result.fields.empty?
- free_raw_result(result)
- result = ActiveRecord::Result.empty(affected_rows: affected_rows)
- end
+ if result.nil?
+ result = ActiveRecord::Result.empty(affected_rows: affected_rows)
+ elsif result.fields.empty?
+ free_raw_result(result)
+ result = ActiveRecord::Result.empty(affected_rows: affected_rows)
+ end
mysql2_adapter.rb のクリーンアップ
activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb から不要になった @affected_rows_before_warnings の初期化行を削除し、コードベースをシンプルにしました。
@@
- @affected_rows_before_warnings = nil
テストの拡張
activerecord/test/cases/adapters/abstract_mysql_adapter/count_deleted_rows_with_lock_test.rb で 期待行数を 13 件に拡張し、複数スレッドでの削除と作成が正しく同期することを検証しています。これにより、affected_rows が正しく伝播するシナリオがテストで保証されます。
@@
- expected_count = 13
+ expected_count = 13
@@
- Bulb.create!(name: "Jimmy", color: "blue")
+ expected_count.times do |i|
+ Bulb.create!(name: "Jimmy #{i}", color: "blue")
+ end
@@
- assert_equal 1, delete_thread.value
+ assert_equal expected_count, delete_thread.value
設計判断
ActiveRecord::Result を affected_rows のみを保持するケースでも再利用する方針 が採用されました。元々 SQLite3Adapter が生結果として Result を利用していることから、Mysql2Adapter でも新規クラスを導入せずに同一型で統一することで API の一貫性とコードの重複回避が実現されています。また、インスタンス変数からローカル変数への移行は スレッド安全性の向上 を意図した設計変更です。
まとめ
この PR は、Mysql2Adapter が affected_rows を正しく取得できない曖昧さを解消し、結果が nil の場合でも行数情報を ActiveRecord::Result にラップして提供します。インスタンス変数の除去とローカル変数への置換によりスレッド安全性が向上し、テストも期待通りの行数を検証できるようになりました。結果として、AR の MySQL 対応が堅牢になり、将来的な回帰リスクが低減します。