PostgreSQLパーティションテーブルの外部キー制約が `NOT VALID` のままになるバグを修正
ActiveRecord::FixtureSet.create_fixtures をPostgreSQLのパーティションテーブルで使用すると、外部キー制約が NOT VALID 状態のまま残留するバグが修正されました。対象は ReferentialIntegrity#check_all_foreign_keys_valid! の UPDATE 文のスコープです。
背景
ActiveRecord::FixtureSet.create_fixtures を実行すると、参照整合性チェックのために check_all_foreign_keys_valid! が呼び出されます。このメソッドは外部キー制約を一時的に convalidated=false(未検証)状態にしてから VALIDATE CONSTRAINT で再検証するというアプローチをとります。しかし、この実装がPostgreSQLのパーティションテーブルと組み合わさったとき、検証済みに戻るべき制約が NOT VALID のまま残るという問題が生じていました。
具体的な症状として、rails db:migrate を実行するたびに structure.sql の外部キー制約に NOT VALID が付与される形で差分が生じ、スキーマファイルが汚染されていました。
PostgreSQLのパーティションテーブルでは、親テーブルと各パーティションがそれぞれ独立したテーブルとして扱われ、同名の外部キー制約を持ちます。UPDATE pg_catalog.pg_constraint を制約名とスキーマ名だけでフィルタリングすると、親テーブルと全パーティションの convalidated が一括で false に設定されます。ところが VALIDATE CONSTRAINT は1テーブルずつ実行されるため、処理済みのテーブル以外のパーティションは NOT VALID のまま取り残されていました。
技術的な変更
UPDATE pg_catalog.pg_constraint に conrelid::regclass によるテーブルスコープを追加することで、1テーブル分の制約のみを NOT VALID に変更するよう修正されました。
変更前:
UPDATE pg_catalog.pg_constraint
SET convalidated=false
WHERE conname = '%1$I'
AND connamespace::regnamespace = '%2$I'::regnamespace;
ALTER TABLE %2$I.%3$I VALIDATE CONSTRAINT %1$I;
変更後:
UPDATE pg_catalog.pg_constraint
SET convalidated=false
WHERE conname = '%1$I'
AND connamespace::regnamespace = '%2$I'::regnamespace
AND conrelid::regclass = '%3$I'::regclass; -- テーブルを限定
ALTER TABLE %2$I.%3$I VALIDATE CONSTRAINT %1$I;
この修正は activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb の1行変更です。%3$I(table_name)は元のSQLにすでに引数として存在していたため、新たな引数追加は不要でした。
テストは activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb に追加されました。親テーブル・パーティション・参照先テーブルの3テーブルを作成して check_all_foreign_keys_valid! を実行し、その後 pg_constraint を直接クエリして convalidated = false の件数がゼロであることを assert_equal で検証しています。
設計判断
UPDATE 文のスコープを最小化するという方針が採用されました。
修正の本質は「テーブル1件分の制約だけを NOT VALID にしてから即座に再検証する」サイクルをテーブルごとに繰り返すことです。conrelid::regclass の条件を追加するだけで、ループ構造や検証ロジック自体には一切手を加えていません。また、パーティション自体もPostgreSQLからは独立したテーブルとして扱われるため、各パーティションは個別にループ処理の対象となり、修正後も正しく検証されます。
変更がSQL文の述語追加1行に収まっていることで、既存の動作(パーティションなしのテーブルや複数スキーマにまたがる外部キー)への影響がなく、リグレッションリスクを最小限に抑えています。
まとめ
SQL述語に conrelid::regclass を追加するだけの1行修正ですが、PostgreSQLのパーティションテーブルで外部キー制約が NOT VALID のまま残留するという根本原因を正確に解消しています。パーティションテーブルを利用するアプリケーションでは、フィクスチャ実行後に structure.sql が汚染される問題が解消されます。