PostgreSQLカラムの等値比較における`generated`型の不整合を修正

rails/rails

PostgreSQLの生成カラムにおいて、#==#hashが異なる抽象レベルで比較していた不整合を修正しました。これにより、eql?/hashコントラクト違反によるHashやSetでのサイレントなバグが解消されます。

背景

PostgreSQL::Column#==#hashが異なるフィールドを参照していたことが、この修正の根本原因です。#==virtual?(booleanに畳み込まれた値)を使用し、#hash@generated(生の文字列"s"または"v")を直接参照していました。Ruby のeql?/hashコントラクトは「a == bならばa.hash == b.hash」を要求するため、この不一致はHashやSetの内部動作を壊す可能性があります。

PostgreSQL 18+では、生成カラムが格納型("s": stored)と仮想型("v": virtual)の2種類を持てるようになりました。virtual?は内部で@generated == "v"と評価されるため、generated: "s"generated: "v"のカラムはどちらもvirtual? == falseとなり、#==では等しいと判定されます。一方で#hash@generatedの生の値を使うため、異なるハッシュ値を返します。この状態でHash/SetのキーとしてColumnオブジェクトを使用すると、「等しいはずのオブジェクトが別のバケットに入る」という予測困難な挙動につながります。

なお、この根本的な不整合はコミット bdc7adce#hashがrawなインスタンス変数を使う形にリファクタリングされた際に、#==が更新されずに生じたものです。同様のバグがSQLite3アダプターでも発見され、#56817で先行して修正されています。

技術的な変更

#==内の比較をvirtual? == other.virtual?からgenerated == other.generatedに置き換え、#hashと同じ抽象レベル(rawな@generated文字列)で比較するようにしました。

変更前:

def ==(other)
  super &&
  identity? == other.identity? &&
  serial? == other.serial? &&
  virtual? == other.virtual?
end

変更後:

def ==(other)
  super &&
  identity? == other.identity? &&
  serial? == other.serial? &&
  generated == other.generated
end

protected
  attr_reader :generated

generatedメソッドはprotectedなアクセサとして定義されています。これによりother.generatedの呼び出しは同クラスのインスタンス間でのみ許可され、クラス外部からの直接アクセスは制限されます。

リグレッションテストとして、test_generated_changes_column_equalityが追加されました。generated: "s"generated: "v"で構築した2つのカラムオブジェクトが、assert_not_equalassert_not_equal ... .hashの両方を通過することを確認しています。

設計判断

virtual?(boolean)からgenerated(raw文字列)へ比較対象を統一するアプローチが採用されました。

別の修正方法として、#hash側をvirtual?を使うよう変更することも考えられます。しかし#hash@generatedで計算することは、将来さらに多くのgenerated型が追加された場合にも自動的に対応できるため、より汎用的です。#==generatedベースに合わせることで、両者の抽象レベルが一致し、将来の型追加時も修正漏れが生じにくい設計になります。

SQLite3の修正(#56817)ではvirtual_stored?の比較を追加する形をとりましたが、PostgreSQLの修正はより根本的な共通フィールドへの統一で対処しており、アダプターごとの実装の違いも見て取れます。

まとめ

#==#hashを同一のフィールド(@generated)で比較するよう統一したことで、Rubyのeql?/hashコントラクトが正しく満たされるようになりました。PostgreSQL 18+の格納型・仮想型という新しい生成カラムの区分に対して、既存のコードが抱えていた潜在的な不整合を、最小限の変更で正確に修正した事例です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
885f6c96

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「総論→各論→結論」の構成が明確であり、リード文、背景、技術的変更、設計判断、まとめの全ての必須要素が適切に配置されています。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

専門知識を持つエンジニアを対象としており、技術レベルや表現が適切です。過度な説明は含まれていません。

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

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

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

Diff内容との照合 ✓ PASS

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

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

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「eql?/hashコントラクト」や「generated column」など、使用されている技術用語はPR情報と一致し、文脈上も正確です。

説明の技術的正確性 ✓ PASS

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

変更の理由や影響に関する技術的な説明は、PR情報とDiffの内容に裏付けられており、論理的で正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiff、関連コミットIDで裏付けられており、ハルシネーション(捏造)は見られません。

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

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

PR番号(#57003)、関連PR番号(#56817)、コミットID(bdc7adce)など、記事に含まれる数値や固有名詞はすべて正確です。

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

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

記事のタイトルはPRの主題「Fix PostgreSQL column equality for generated types」を正確に反映しています。

外部知識の正確性 ✓ PASS

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

記事に含まれる情報はすべてPR情報に由来しており、サポート状況やリリース日程など、PRに記載のない外部知識の追加はありません。

時間表現の正確性 ✓ PASS

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

「生じた」「先行して修正された」などの時間表現は、PRの文脈と一致しており、正確です。