Skip to content

Fix find_signed for models with a composite primary key - #57245

Merged
fatkodima merged 1 commit into
rails:mainfrom
55728:fix-find-signed-composite-primary-key
Apr 25, 2026
Merged

fatkodima merged 1 commit into
rails:mainfrom
55728:fix-find-signed-composite-primary-key

Conversation

@55728

@55728 55728 commented Apr 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ActiveRecord::SignedId::ClassMethods#find_signed raises an ArgumentError for any model that declares a composite primary key (e.g. self.primary_key = [:shop_id, :id]). The bang variant find_signed! works correctly. This PR makes find_signed consistent with find_signed! and find.

The bug

class Cpk::Order < ActiveRecord::Base
  self.primary_key = [:shop_id, :id]
end

order = Cpk::Order.first
token = order.signed_id

Cpk::Order.find_signed!(token)
# => #<Cpk::Order id: 75294940, shop_id: 37647470, ...>

Cpk::Order.find_signed(token)
# => ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array

Hits anyone using find_signed on a CPK model — for example, a magic sign-in link, password reset, or email confirmation that scopes its token to such a record.

Root cause

find_signed calls

find_by primary_key => id

at signed_id.rb. For composite primary keys, primary_key is an array (["shop_id", "id"]) and the verified id is also an array of values ([37647470, 75294940]). The predicate builder treats an array key as a request for multiple records and expects the value to be an array of arrays ([[shop_id, id], [shop_id, id], ...]). It iterates the inner values, finds non-arrays, and raises:

predicate_builder.rb:

ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array

find_signed! does not hit this because it uses find(id), which dispatches to find_one and zips primary_key with id for CPK at finder_methods.rb.

Note

Prior art

The same class of bug was fixed for find_by_token_for in 8617a7c (September 2024, by @fatkodima), where find_by(model.primary_key => id) was changed to find_by(model.primary_key => [id]) in token_for.rb. signed_id.rb has the identical pattern but was not updated at that time.

Fix

Mirror the find_one pattern in find_signed:

if id = signed_id_verifier.verified(...)
  if composite_primary_key?
    find_by(primary_key.zip(id).to_h)
  else
    find_by(primary_key => id)
  end
end

find_by (rather than find) is preserved so relation scoping via SignedId::RelationMethods#find_signed still works (e.g. Cpk::Order.where(...).find_signed(token)).

Note

Why zip instead of wrapping in an array?

The token_for.rb fix wraps the value in an extra array (find_by(primary_key => [id])), which works because the predicate builder interprets it as "find one record matching this composite key tuple." This PR instead uses primary_key.zip(id).to_h, which produces a plain conditions hash (e.g. { shop_id: 37647470, id: 75294940 }).

I chose the zip approach because:

  1. It mirrors the pattern already used in find_one (finder_methods.rb), keeping the CPK handling consistent across the codebase.
  2. The intent is immediately clear — each key column is explicitly paired with its value — whereas [id] requires understanding how the predicate builder disambiguates single-tuple vs multi-record lookups.

Either approach produces the same query; this is purely a readability choice.

Tests

Three new tests in signed_id_test.rb cover:

  • find_signed round trip with CPK
  • find_signed on a relation with CPK (positive + negative scope)
  • find_signed! round trip with CPK (regression guard)

Added a CHANGELOG entry.

Without the fix, the first two fail with the ArgumentError shown above; with the fix, the full file (38 tests, 56 assertions) passes. CI will exercise MySQL and PostgreSQL as well.

@fatkodima fatkodima self-assigned this Apr 25, 2026
Previously, `find_signed` raised
`ArgumentError: Expected corresponding value for [...] to be an Array`
for any model declaring a composite primary key (e.g.
`self.primary_key = [:shop_id, :id]`). The verified id array was passed
to `find_by(primary_key => id)`, which the predicate builder interprets
as a request for multiple records and expects an array of arrays.

`find_signed!` already worked because it dispatches through `find`,
which zips the primary key with the id for composite keys via
`find_one`. This brings `find_signed` in line with `find_signed!` and
`find` while preserving relation scoping (so
`Model.where(...).find_signed(token)` keeps working).
@fatkodima
fatkodima force-pushed the fix-find-signed-composite-primary-key branch from a925a74 to 5070fbb Compare April 25, 2026 17:16
@fatkodima
fatkodima merged commit 20970df into rails:main Apr 25, 2026
3 of 4 checks passed
fatkodima added a commit that referenced this pull request Apr 25, 2026
…y-key

Fix `find_signed` for models with a composite primary key
fatkodima added a commit that referenced this pull request Apr 25, 2026
…y-key

Fix `find_signed` for models with a composite primary key
@55728

55728 commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants