PostgreSQL 18.4+向け外部キーの `enforced:` オプションを追加

rails/rails

Active Recordの add_foreign_keychange_foreign_keyenforced: オプションが追加され、PostgreSQL 18.4以降で外部キー制約の参照整合性チェックをスーパーユーザー権限なしに制御できるようになりました。

背景

PostgreSQL 17以前では、外部キーチェックを一時的に無効化するには DISABLE TRIGGER ALL を発行する必要があり、スーパーユーザー権限が必要でした。さらにトリガーを再有効化しても既存の不正データは再検証されないため、データの整合性が静かに崩れるリスクがありました。

PostgreSQL 18.0でこの問題を解決する NOT ENFORCED オプションが外部キー制約に導入されました。テーブルオーナーが権限なしに切り替えでき、ENFORCED に戻す際は全行の再検証が行われます。ただし、PostgreSQL 18.0〜18.3には「NOT ENFORCED 外部キーで DEFERRABLE 属性が失われる」バグが存在しており、Railsが依存する遅延制約の仕組みが壊れる問題がありました。このバグはPostgreSQL 18.4で修正されたため(「Fix loss of deferrability of foreign-key triggers」)、Active Recordはバージョン18.4以降を要件としています。

なお、以前にスーパーユーザー権限不要の手段として SET CONSTRAINTS ALL DEFERRED を利用するアプローチが試みられましたが、NO ACTION 以外の参照アクションは DEFERRABLE 宣言があっても遅延できないため、2812694 でリバートされた経緯があります。

技術的な変更

enforced: オプションはスキーマ定義・SQL生成・スキーマダンプ・バージョンチェックの4層にわたって実装されています。

ForeignKeyDefinitionへのメソッド追加

schema_definitions.rbenforced? メソッドが追加されました。options[:enforced] が未指定の場合は true をデフォルト返す実装で、既存の外部キーへの影響はありません。

def enforced?
  options.fetch(:enforced, true)
end

SQL生成

PostgreSQLの visit_ForeignKeyDefinitionNOT ENFORCED 句の追記が加わりました。enforced?false のときのみ付加されるため、既存のDDL出力は変わりません。

def visit_ForeignKeyDefinition(o)
  super.dup.tap do |sql|
    sql << " DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}" if o.deferrable
    sql << " NOT ENFORCED" unless o.enforced?
  end
end

バージョンチェックと change_foreign_key の追加

postgresql_adapter.rbsupports_enforced_foreign_keys? が実装され、database_version >= 18_00_04 を要件とします。add_foreign_key はこのチェックを呼び出し、要件未満のバージョンでは ArgumentError を発生させます。また、抽象アダプターに change_foreign_key のデフォルト実装(NotImplementedError)が追加され、PostgreSQLアダプターがオーバーライドする形で ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED / NOT ENFORCED を発行します。

def supports_enforced_foreign_keys?
  database_version >= 18_00_04
end

foreign_keys メソッドとスキーマダンプ

foreign_keys の内部クエリは supports_enforced_foreign_keys? が真のとき c.conenforced AS enforced カラムを動的に追加します。スキーマダンパーは enforced?false のときのみ enforced: false を出力します。PostgreSQLは NOT ENFORCED 制約を内部的に NOT VALID として扱うため、ダンプには enforced: falsevalidate: false の両方が出力されます。

parts << "enforced: #{foreign_key.enforced?.inspect}" unless foreign_key.enforced?

設計判断

バージョン条件を supports_enforced_foreign_keys? メソッドに集約することで、将来の disable_referential_integrity 対応(フォローアップPRで予定)でも同じ判定ロジックを再利用できる設計になっています。

change_foreign_key を抽象アダプターに NotImplementedError として追加したことも重要な判断です。これにより、他のアダプターへの将来の拡張に向けたインターフェース定義が確保されつつ、現時点ではPostgreSQLアダプターのみが実装を持ちます。ドキュメントには「マイグレーションコマンドではなくランタイムヘルパー」として明記されており、可逆マイグレーションとして登録されていないことが意図的に示されています。

NOT ENFORCED 制約がPostgreSQLによって内部的に NOT VALID として扱われる点は、Active Record側での追加ハンドリングを必要とせず、validate: false として自然に表現されます。スキーマダンプが両フラグを出力することで、ダンプから復元した際も同じ状態が再現されます。

まとめ

本PRは、PostgreSQL 18.4のバグ修正を前提に、スーパーユーザー権限なしで外部キーの参照整合性チェックを制御する手段をActive Recordに組み込んだ変更です。バージョンチェック・SQL生成・スキーマダンプを一貫して拡張することで、enforced: false の外部キーをRailsのマイグレーション体系の中で安全に扱えるようになりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
5e52a179

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)、背景・技術的な変更・設計判断(各論)、まとめ(結論)という3部構成が明確に適用されており、模範的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト、PR番号・コミットIDのリンク記法など、全てのカスタムMarkdown構文が正しく使用されています。

対象読者への適合性 ✓ PASS

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

専門用語が適切に使用されており、内容は専門知識を持つエンジニア向けに最適化されています。過度な初心者向けの解説はありません。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。非常に可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロックは、提供されたDiffの内容と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「NOT ENFORCED」「DEFERRABLE」「conenforced」など、PostgreSQLおよびActive Recordの技術用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

PostgreSQLのバージョンごとの挙動の違いや、`NOT ENFORCED`が内部的に`NOT VALID`として扱われる点など、技術的な説明はPR情報に基づき正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張はPRのDescriptionやDiffの内容によって裏付けられており、推測や憶測に基づくハルシネーションは見られません。

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

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

PR番号(#57377)、コミットID(2812694)、PostgreSQLのバージョン番号(18.4など)といった数値や固有名詞はすべて正確です。

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

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

記事のタイトルはPRのタイトルを正確に反映しており、記事全体の内容もPRの主題と完全に一致しています。

外部知識の正確性 ✓ PASS

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

記事で言及されているPostgreSQLのバージョン情報やバグ修正の歴史は、すべてPR情報内で言及されている内容であり、外部からの不正確な知識の持ち込みはありません。

時間表現の正確性 ✓ PASS

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

「以前は」「導入されました」「予定」といった時間表現は、PR情報と整合性が取れており、事実関係を正確に伝えています。