Skip to content

Commit 4435b7a

Browse files
authored
Merge pull request rails#46503 from joshuay03/update-calculations-ids
Fix: `ActiveRecord::Calculations#ids` returns duplicate ids
2 parents cc50791 + 8fd3369 commit 4435b7a

File tree

5 files changed

+106
-8
lines changed

5 files changed

+106
-8
lines changed

activerecord/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
* Return only unique ids from ActiveRecord::Calculations#ids
2+
3+
Updated ActiveRecord::Calculations#ids to only return the unique ids of the base model
4+
when using eager_load, preload and includes.
5+
6+
```ruby
7+
Post.find_by(id: 1).comments.count
8+
# => 5
9+
Post.includes(:comments).where(id: 1).pluck(:id)
10+
# => [1, 1, 1, 1, 1]
11+
Post.includes(:comments).where(id: 1).ids
12+
# => [1]
13+
```
14+
15+
*Joshua Young*
16+
117
* Stop using `LOWER()` for case-insensitive queries on `citext` columns
218

319
Previously, `LOWER()` was added for e.g. uniqueness validations with

activerecord/lib/active_record/querying.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module Querying
1717
:and, :or, :annotate, :optimizer_hints, :extending,
1818
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
1919
:count, :average, :minimum, :maximum, :sum, :calculate,
20-
:pluck, :pick, :ids, :strict_loading, :excluding, :without, :with,
20+
:pluck, :pick, :ids, :async_ids, :strict_loading, :excluding, :without, :with,
2121
:async_count, :async_average, :async_minimum, :async_maximum, :async_sum, :async_pluck, :async_pick,
2222
].freeze # :nodoc:
2323
delegate(*QUERYING_METHODS, to: :all)

activerecord/lib/active_record/relation/calculations.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,33 @@ def async_pick(*column_names)
317317
async.pick(*column_names)
318318
end
319319

320-
# Pluck all the ID's for the relation using the table's primary key
320+
# Returns the base model's ID's for the relation using the table's primary key
321321
#
322322
# Person.ids # SELECT people.id FROM people
323323
# Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
324324
def ids
325-
pluck primary_key
325+
if loaded?
326+
result = records.pluck(primary_key)
327+
return @async ? Promise::Complete.new(result) : result
328+
end
329+
330+
columns = arel_columns([primary_key])
331+
relation = spawn
332+
relation.select_values = columns
333+
result = if relation.where_clause.contradiction?
334+
ActiveRecord::Result.empty
335+
else
336+
skip_query_cache_if_necessary do
337+
klass.connection.select_all(relation, "#{klass.name} Ids", async: @async)
338+
end
339+
end
340+
341+
result.then { |result| type_cast_pluck_values(result, columns) }
342+
end
343+
344+
# Same as <tt>#ids</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
345+
def async_ids
346+
async.ids
326347
end
327348

328349
private

activerecord/test/cases/calculations_test.rb

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -928,8 +928,69 @@ def test_pluck_with_selection_clause
928928
assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit))"))
929929
end
930930

931-
def test_plucks_with_ids
932-
assert_equal Company.all.map(&:id).sort, Company.ids.sort
931+
def test_ids
932+
assert_equal Company.all.map(&:id).sort, Company.all.ids.sort
933+
end
934+
935+
def test_ids_with_scope
936+
scoped_ids = [1, 2]
937+
assert_equal Company.where(id: scoped_ids).map(&:id).sort, Company.where(id: scoped_ids).ids.sort
938+
end
939+
940+
def test_ids_on_loaded_relation
941+
loaded_companies = Company.all.load
942+
company_ids = Company.all.map(&:id)
943+
assert_queries(0) do
944+
assert_equal company_ids.sort, loaded_companies.ids.sort
945+
end
946+
end
947+
948+
def test_ids_on_loaded_relation_with_scope
949+
scoped_ids = [1, 2]
950+
loaded_companies = Company.where(id: scoped_ids).load
951+
company_ids = Company.where(id: scoped_ids).map(&:id)
952+
assert_queries(0) do
953+
assert_equal company_ids.sort, loaded_companies.ids.sort
954+
end
955+
end
956+
957+
def test_ids_async_on_loaded_relation
958+
loaded_companies = Company.all.order(:id).load
959+
assert_async_equal loaded_companies.ids, loaded_companies.async_ids
960+
end
961+
962+
def test_ids_with_contradicting_scope
963+
empty_scope_ids = []
964+
company_ids = Company.where(id: empty_scope_ids).map(&:id)
965+
assert_predicate company_ids, :empty?
966+
assert_queries(0) do
967+
assert_equal company_ids, Company.where(id: empty_scope_ids).ids
968+
end
969+
end
970+
971+
def test_ids_with_eager_load
972+
company = Company.first
973+
5.times { company.contracts.create! }
974+
assert_equal Company.all.map(&:id).sort, Company.all.eager_load(:contracts).ids.sort
975+
end
976+
977+
def test_ids_with_preload
978+
company = Company.first
979+
5.times { company.contracts.create! }
980+
assert_equal Company.all.map(&:id).sort, Company.all.preload(:contracts).ids.sort
981+
end
982+
983+
def test_ids_with_includes
984+
company = Company.first
985+
5.times { company.contracts.create! }
986+
assert_equal Company.all.map(&:id).sort, Company.all.includes(:contracts).ids.sort
987+
end
988+
989+
def test_ids_with_scope_and_includes
990+
scoped_ids = [1, 2]
991+
company = Company.where(id: scoped_ids).first
992+
5.times { company.contracts.create! }
993+
assert_equal Company.where(id: scoped_ids).map(&:id).sort, Company.where(id: scoped_ids).includes(:contracts).ids.sort
933994
end
934995

935996
def test_pluck_with_includes_limit_and_empty_result

activerecord/test/cases/relation_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,9 @@ def test_relation_with_merged_joins_aliased_works
256256
posts_with_joins_and_merges = Post.joins(:author, :categorizations)
257257
.merge(Author.select(:id)).merge(categorizations_with_authors)
258258

259-
author_with_posts = Author.joins(:posts).ids
260-
categorizations_with_author = Categorization.joins(:author).ids
261-
posts_with_author_and_categorizations = Post.joins(:categorizations).where(author_id: author_with_posts, categorizations: { id: categorizations_with_author }).ids
259+
author_with_posts = Author.joins(:posts).pluck(:id)
260+
categorizations_with_author = Categorization.joins(:author).pluck(:id)
261+
posts_with_author_and_categorizations = Post.joins(:categorizations).where(author_id: author_with_posts, categorizations: { id: categorizations_with_author }).pluck(:id)
262262

263263
assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.count
264264
assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.to_a.size

0 commit comments

Comments
 (0)