Fix PostgreSQL foreign_keys handling for quoted schemas
PostgreSQL アダプタの foreign_keys メソッドが、引用されたスキーマ名を含むテーブルを参照した際に to_table を誤って加工し、rails db:schema:dump が壊れた add_foreign_key を出力していた問題を解消します。これにより、スキーマダンプおよびマイグレーションが正しく動作します。
背景
foreign_keys が quoted スキーマで to_table を破損する のは、Rails が PostgreSQL の内部表現 regclass::text を単純な文字列操作でデクォートしていたためです。regclass::text はスキーマ修飾かつ部分的に引用された文字列(例: "App".customers)を返し、Utils.unquote_identifier は先頭と末尾の 1 文字だけを除去するロジックだったため、途中の引用が残ったまま App".customers という不正な名前が生成されました。
この不整合は、rails db:schema:dump が外部キー定義を再生成する際に破損した文字列をそのまま書き出すことで顕在化し、ダンプファイルをロードすると add_foreign_key が失敗します。さらに、マイグレーションやテスト環境でのスキーマ操作が期待通りに行えないという実務的な障害につながっていました。
問題の根本は「単一識別子向けのデクォートロジックをスキーマ修飾名に流用した」ことにあります。 このロジックは多くのケースで正しく機能しますが、引用を含むスキーマ名だけが例外となり、テストカバレッジが不足していたために長期間見過ごされてきました。
技術的な変更
foreign_keys 実装が Utils.unquote_identifier から Utils.extract_schema_qualified_name へ置き換えられました。 後者はスキーマ修飾名と引用を正確に解析できる既存ユーティリティで、to_table の文字列を安全に正規化します。
@@
- to_table = Utils.unquote_identifier(row["to_table"])
+ to_table = Utils.extract_schema_qualified_name(row["to_table"]).to_s
この一行差し替えにより、"Aerospace".boosters のような名前は Aerospace.boosters に正しく変換され、foreign_keys が返すオブジェクトの to_table が期待通りの文字列になります。
さらに、PostgreSQL 固有のテストケースが追加され、引用スキーマ内で外部キーを作成し to_table が正しいことを検証します。
@@
+ def test_foreign_key_to_table_in_quoted_schema
+ @connection.execute('DROP SCHEMA IF EXISTS "Aerospace" CASCADE')
+ @connection.execute('CREATE SCHEMA "Aerospace"')
+ @connection.execute('CREATE TABLE "Aerospace".boosters (id serial primary key)')
+ @connection.add_column :astronauts, :booster_id, :integer
+ @connection.execute(<<~SQL)
+ ALTER TABLE astronauts
+ ADD CONSTRAINT fk_booster FOREIGN KEY (booster_id) REFERENCES "Aerospace".boosters(id)
+ SQL
+
+ fk = @connection.foreign_keys("astronauts").find { |k| k.name == "fk_booster" }
+ assert_not_nil fk
+ assert_equal "Aerospace.boosters", fk.to_table
+ ensure
+ @connection.execute('DROP SCHEMA IF EXISTS "Aerospace" CASCADE')
+ end
テストは PostgreSQL 17 でもグリーンとなり、実装変更が正しく機能することが自動的に保証されます。
設計判断
既存ユーティリティ Utils.extract_schema_qualified_name を再利用した点が重要です。 新規関数を導入せず、すでにスキーマ修飾名の解析を担っているコードを活用することで、変更箇所を最小限に抑えつつ一貫性を保ちました。
この選択は後方互換性にも配慮しています。foreign_keys が従来通り true/false のような単純文字列を受け取るケースでは動作に影響がなく、引用なしのテーブル名はそのまま返ります。したがって、既存アプリケーションが即座に壊れるリスクはなく、デフォルトの挙動を保持したままバグだけを除去しています。
副作用の範囲は PostgreSQL アダプタに限定 され、他のデータベースアダプタや ActiveRecord のコアロジックには変更が加わっていません。これにより、リリース全体へのリスクが低く、限定的なテストで十分に検証できる設計となっています。
まとめ
本 PR は、PostgreSQL の foreign_keys が quoted スキーマを正しく処理できなかった根本原因を Utils.extract_schema_qualified_name への置き換えで解消し、テストも追加して回帰を防止しました。変更は局所的かつ後方互換性を保ちつつ、スキーマダンプやマイグレーションの信頼性を向上させます。