Composite primary key support added to `delete`
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.