Composite primary key support added to `delete`

rails/rails

ActiveRecord now allows Model.delete to accept a single composite primary key, aligning its behavior with Model.destroy and eliminating a long‑standing parity gap.

背景

delete raised an ArgumentError when it received a single ID for a model that uses a composite primary key, while its documented counterpart destroy successfully handled the same input. The failure manifested as a malformed tuple error because the method forwarded the raw argument to where(model.primary_key => id_or_array). Existing tests already confirmed that destroy works with a single composite key, demonstrating that the underlying query builder can process such values when correctly wrapped. Consequently, developers faced an inconsistent API where destroy could be used but delete could not, contradicting the documentation that presents the two methods as interchangeable.

技術的な変更

The fix introduces a small guard clause inside ActiveRecord::Relation#delete that detects a composite primary key and ensures a single ID is wrapped in an outer array before constructing the predicate. This mirrors the logic already present in destroy, allowing the same code path to handle both single and batch deletions.

@@
-      where(model.primary_key => id_or_array).delete_all
+      if model.composite_primary_key? && !id_or_array.first.is_a?(Array)
+        id_or_array = [id_or_array]
+      end
+
+      where(model.primary_key => id_or_array).delete_all

The added condition checks model.composite_primary_key? and whether the first element of the argument is already an array; if not, it wraps the argument, turning a single composite key like ["author_id", "id"] into [["author_id", "id"]]. This transformation satisfies the where clause’s expectation of an array‑of‑arrays for composite keys, preventing the previous ArgumentError.

Accompanying test cases verify the new behavior: one test asserts that Cpk::Book.delete(book.id) deletes a single record, and another confirms that passing an array of composite IDs continues to delete multiple records correctly.

@@
-  def test_delete_with_single_composite_primary_key
-    book = cpk_books(:cpk_great_author_first_book)
-
-    assert_difference("Cpk::Book.count", -1) do
-      assert_equal 1, Cpk::Book.delete(book.id)
-    end
-  end
+  def test_delete_with_single_composite_primary_key
+    book = cpk_books(:cpk_great_author_first_book)
+
+    assert_difference("Cpk::Book.count", -1) do
+      assert_equal 1, Cpk::Book.delete(book.id)
+    end
+  end
@@
-  def test_delete_with_multiple_composite_primary_keys
-    books = [
-      cpk_books(:cpk_great_author_first_book),
-      cpk_books(:cpk_great_author_second_book),
-    ]
-
-    assert_difference("Cpk::Book.count", -2) do
-      assert_equal 2, Cpk::Book.delete(books.map(&:id))
-    end
-  end
+  def test_delete_with_multiple_composite_primary_keys
+    books = [
+      cpk_books(:cpk_great_author_first_book),
+      cpk_books(:cpk_great_author_second_book),
+    ]
+
+    assert_difference("Cpk::Book.count", -2) do
+      assert_equal 2, Cpk::Book.delete(books.map(&:id))
+    end
+  end

設計判断

The core design decision was to wrap a single composite ID in an array before delegating to the existing where logic, rather than rewriting the predicate construction. This mirrors the approach already taken by destroy, ensuring consistent handling across both methods while keeping the change localized to a few lines.

By conditioning the wrap on model.composite_primary_key?, the patch leaves the single‑primary‑key code path untouched, preserving the performance characteristics for the common case. The batch form delete([id1, id2]) also remains unchanged because its first element is already an array, so existing callers see no behavioral regression.

Overall, the modification is minimally invasive: it adds a guard clause without altering method signatures, core query generation, or external APIs. Maintaining backward compatibility was a priority, and the solution achieves parity with destroy without introducing new configuration knobs.

まとめ

ActiveRecord now treats Model.delete with a single composite primary key identically to Model.destroy, thanks to a simple array‑wrapping guard. This resolves the documented inconsistency, retains existing behavior for simple keys and batch deletions, and does so with a low‑risk, backward‑compatible change.

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
19e19b40

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断、まとめがすべて揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい `言語:ファイルパス` 形式ですが、PR リンクが `[PR #57657](URL)` となっており、要求された `[#57657](URL)` 形式になっていません。

対象読者への適合性 ✓ PASS

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

対象は Rails 開発者向けで、専門用語が適切に使われており、初心者向けの余計な説明はありません。

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

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

各セクションは総論・各論・結論の構成を保ち、段落はトピックセンテンスで始まり、1段落1トピック、長さも適切です。

Diff内容との照合 ⚠ WARNING

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

Relation の変更は Diff と一致していますが、テストファイルの diff 表示が削除行(-)として示されており、実際の PR では追加のみです。コード内容は正しいものの、Diff 表記が正確ではありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「composite primary key」「guard clause」などの用語は正しく使用され、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

変更理由、実装方法、影響について PR の記載と矛盾せず、技術的に正確です。

事実の突合 ✓ PASS

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

記事のすべての主張は PR タイトル・説明・Diff で裏付けられており、推測や捏造はありません。

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

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

PR 番号 #57657 の記載は正確です。その他数値や固有名詞の誤りはありません。

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

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

記事タイトルは PR の趣旨を正確に伝えており、重要な意味のずれはありません。

外部知識の正確性 ✓ PASS

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

PR 以外のバージョンサポート情報やリリース日等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現は使用されておらず、PR の記載と矛盾する箇所はありません。