仮想カラムと通常カラムの重複排除を分離し、データ損失を防止

rails/rails

SQLite3とPostgreSQLの Column クラスにおいて、virtual? 属性が ==hash メソッドに含まれるようになりました。これにより、GENERATED(仮想)カラムと通常カラムが誤って同一視されることがなくなり、INSERT/UPDATE時のデータ損失が防止されます。

背景

Active Recordの Deduplicable モジュールは、==hash を使用してカラムインスタンスのレジストリを管理しています。同じ名前と型を持つカラムは重複排除の対象となり、メモリ効率化が図られています。

#56795 で報告された問題では、GENERATEDカラムが先にレジストリに登録されると、別テーブルの同名・同型の通常カラムがそのGENERATEDカラムと重複排除されてしまう事象が発生していました。GENERATEDカラムはINSERT/UPDATEから除外される性質を持つため、通常カラムが誤ってGENERATEDカラムと同一視されると、その通常カラムへの書き込みが暗黙的に無視され、データベースにNULL値が保存される深刻なデータ損失につながります。

問題の根本原因は、SQLite3::ColumnPostgreSQL::Column の等価性判定が virtual? 属性を考慮していなかったことです。名前・型・その他の属性が一致すれば、GENERATEDかどうかに関わらず同一のカラムとして扱われていました。

技術的な変更

activerecord/lib/active_record/connection_adapters/sqlite3/column.rbactiverecord/lib/active_record/connection_adapters/postgresql/column.rb において、== メソッドと hash メソッドに virtual? の比較が追加されました。

SQLite3::Column の変更前:

def ==(other)
  other.is_a?(Column) &&
    super &&
    auto_increment? == other.auto_increment?
end

def hash
  Column.hash ^
    super.hash ^
    auto_increment?.hash ^
    rowid.hash
end

SQLite3::Column の変更後:

def ==(other)
  other.is_a?(Column) &&
    super &&
    auto_increment? == other.auto_increment? &&
    virtual? == other.virtual?
end

def hash
  Column.hash ^
    super.hash ^
    auto_increment?.hash ^
    rowid.hash ^
    virtual?.hash
end

PostgreSQLの Column クラスにも同様の変更が適用されています。== メソッドでは virtual? == other.virtual? の条件が追加され、hash メソッドでは virtual?.hash がXOR演算に含まれるようになりました。

両アダプタのテストファイル(activerecord/test/cases/adapters/sqlite3/virtual_column_test.rbactiverecord/test/cases/adapters/postgresql/virtual_column_test.rb)に、仮想カラムと通常カラムが重複排除されないことを検証する test_virtual_column_does_not_deduplicate_with_regular_column が追加されています。このテストでは、仮想カラムを持つテーブルのスキーマを先に読み込んでからDeduplicableレジストリに登録させ、その後に同名の通常カラムを持つテーブルを作成しています。

def test_virtual_column_does_not_deduplicate_with_regular_column
  @connection.create_table :regular_columns, force: true do |t|
    t.string :name
  end

  regular_model = Class.new(ActiveRecord::Base) { self.table_name = "regular_columns" }

  # Ensure deduplication has seen the virtual column first
  VirtualColumn.columns_hash

  regular_col = regular_model.columns_hash["name"]
  virtual_col = VirtualColumn.columns_hash["upper_name"]

  assert_predicate virtual_col, :virtual?
  assert_not_predicate regular_col, :virtual?
  assert_not_equal regular_col, virtual_col

  # The regular column must not be excluded from inserts
  record = regular_model.create!(name: "hello")
  record.reload
  assert_equal "hello", record.name
ensure
  @connection.drop_table :regular_columns, if_exists: true
end

テストは、2つのカラムが等価でないこと(assert_not_equal)と、通常カラムへの書き込みが正常に保存されること(assert_equal "hello", record.name)の両方を検証しています。

設計判断

virtual? を等価性判定に含める方式 が採用されました。

このアプローチは、カラムの重複排除ロジックそのものには手を加えず、カラムの同一性を判定する条件を厳密化する設計です。Deduplicable モジュールは引き続き ==hash に依存し、各アダプタの Column クラスが自身の特性に応じた等価性定義を提供します。

SQLite3では auto_increment?rowid、PostgreSQLでは identity?serial? など、アダプタ固有の属性がすでに等価性判定に含まれていました。virtual? もこれらと同様に、カラムの振る舞いを決定する重要な属性として、等価性判定の一部に組み込まれています。

hash メソッドへの追加も同様の一貫性を保っています。Rubyの Hash クラスがキーの等価性判定に eql?hash の両方を使用するため、== で比較される属性は hash にも反映される必要があります。XOR演算(^)によって各属性のハッシュ値を組み合わせる既存のパターンを踏襲し、virtual?.hash が追加されました。

まとめ

本PRは、カラムの等価性判定に virtual? 属性を追加することで、GENERATEDカラムと通常カラムの誤った重複排除を防いでいます。==hash の両方に一貫して virtual? を含めることで、Deduplicableレジストリが異なる generated_type を持つカラムを正しく区別し、データ損失のリスクを排除しました。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)およびGitHubのIssue/PRリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Active Recordの内部実装に関する知識を前提としており、専門知識を持つエンジニアという対象読者に完全に適合しています。過度な説明はありません。

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

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

各セクション、各パラグラフが「総論→各論」の原則に従っています。特に、各パラグラフの先頭にトピックセンテンスが置かれているため、見出しと各段落の1文目だけで記事の要点を把握できます。1段落1トピックも守られています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロック(SQLite3の変更前後、テストコード)は、提供されたDiff情報と完全に一致しています。PostgreSQLの変更についても、コードを省略しつつ正確に説明できています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Deduplicable」「GENERATED(仮想)カラム」「XOR演算」などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

仮想カラムと通常カラムが誤って重複排除される問題、それがデータ損失に繋がるメカニズム、そして`virtual?`を等価性判定に加える解決策について、技術的に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の背景、技術的変更点、関連Issue番号など)は、提供されたPRのDescriptionやDiff情報によって裏付けられています。特に「設計判断」セクションは、コードの変更意図を的確に解釈しており、ハルシネーションは見られません。

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

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

PR番号(#56797)とIssue番号(#56795)が正確に記載されています。

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

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

記事のタイトル「仮想カラムと通常カラムの重複排除を分離し、データ損失を防止」は、PRが解決する問題とその影響を的確に要約しており、PR内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

記事には、PR情報に基づかない外部知識(バージョン情報、リリース予定など)は一切含まれていません。

時間表現の正確性 ✓ PASS

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

記事内の時間表現は、PRの文脈と一致しており、事実を歪曲するような記述はありません。