Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
rename Relation#uniq to Relation#distinct. #uniq still works.
The similarity of `Relation#uniq` to `Array#uniq` is confusing. Since our
Relation API is close to SQL terms I renamed `#uniq` to `#distinct`.

There is no deprecation. `#uniq` and `#uniq!` are aliases and will continue
to work. I also updated the documentation to promote the use of `#distinct`.
  • Loading branch information
senny committed Mar 15, 2013
1 parent bfee706 commit a1bb6c8
Show file tree
Hide file tree
Showing 22 changed files with 86 additions and 45 deletions.
7 changes: 7 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,5 +1,12 @@
## Rails 4.0.0 (unreleased) ##

* Rename `Relation#uniq` to `Relation#distinct`. `#uniq` is still
available as an alias but we encourage to use `#distinct` instead.
Also `Relation#uniq_value` is aliased to `Relation#distinct_value`,
this is a temporary solution and you should migrate to `distinct_value`.

*Yves Senn*

* Fix quoting for sqlite migrations using copy_table_contents() with binary
columns.

Expand Down
1 change: 1 addition & 0 deletions activerecord/lib/active_record/associations.rb
Expand Up @@ -241,6 +241,7 @@ def association_instance_set(name, association)
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
# others.exists? | X | X | X
# others.distinct | X | X | X
# others.uniq | X | X | X
# others.reset | X | X | X
#
Expand Down
Expand Up @@ -174,7 +174,7 @@ def count(column_name = nil, count_options = {})

reflection.klass.count_by_sql(custom_counter_sql)
else
if association_scope.uniq_value
if association_scope.distinct_value
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
column_name ||= reflection.klass.primary_key
count_options[:distinct] = true
Expand Down Expand Up @@ -246,14 +246,14 @@ def destroy(*records)
# +count_records+, which is a method descendants have to provide.
def size
if !find_target? || loaded?
if association_scope.uniq_value
if association_scope.distinct_value
target.uniq.size
else
target.size
end
elsif !loaded? && !association_scope.group_values.empty?
load_target.size
elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array)
elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
unsaved_records = target.select { |r| r.new_record? }
unsaved_records.size + count_records
else
Expand Down Expand Up @@ -306,12 +306,13 @@ def many?
end
end

def uniq
def distinct
seen = {}
load_target.find_all do |record|
seen[record.id] = true unless seen.key?(record.id)
end
end
alias uniq distinct

# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
Expand Down Expand Up @@ -352,7 +353,7 @@ def add_to_target(record)
callback(:before_add, record)
yield(record) if block_given?

if association_scope.uniq_value && index = @target.index(record)
if association_scope.distinct_value && index = @target.index(record)
@target[index] = record
else
@target << record
Expand Down
Expand Up @@ -649,11 +649,12 @@ def destroy(*records)
# # #<Pet name: "Fancy-Fancy">
# # ]
#
# person.pets.select(:name).uniq
# person.pets.select(:name).distinct
# # => [#<Pet name: "Fancy-Fancy">]
def uniq
@association.uniq
def distinct
@association.distinct
end
alias uniq distinct

# Count all records using SQL.
#
Expand Down
Expand Up @@ -6,7 +6,7 @@ class HasManyThrough < CollectionAssociation #:nodoc:

def associated_records_by_owner
super.each do |owner, records|
records.uniq! if reflection_scope.uniq_value
records.uniq! if reflection_scope.distinct_value
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/querying.rb
Expand Up @@ -8,7 +8,7 @@ module Querying
delegate :find_each, :find_in_batches, :to => :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :references, :none, :to => :all
:having, :create_with, :uniq, :distinct, :references, :none, :to => :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all

# Executes a custom SQL query against your database and returns all the results. The results will
Expand Down
8 changes: 7 additions & 1 deletion activerecord/lib/active_record/relation.rb
Expand Up @@ -10,7 +10,7 @@ class Relation
:extending]

SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
:reverse_order, :uniq, :create_with]

This comment has been minimized.

Copy link
@codeodor

codeodor Mar 31, 2013

Contributor

@senny Removing :uniq in favor of :distinct causes a build problem for activerecord-deprecated_finders (see rails/activerecord-deprecated_finders#8).

Do you know if that should be fixed in AR-deprecated_finders or in Rails itself?

This comment has been minimized.

Copy link
@codeodor

codeodor Mar 31, 2013

Contributor

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Apr 1, 2013

Member

Fixed at c311447

This comment has been minimized.

Copy link
@senny

senny Apr 2, 2013

Author Member

Sorry for my late response, I was off a couple of days. Thanks @rafaelfranca for the fix! ❤️

:reverse_order, :distinct, :create_with]

VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS

Expand Down Expand Up @@ -506,6 +506,12 @@ def joined_includes_values
includes_values & joins_values
end

# +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
# to maintain backwards compatibility. Use +distinct_value+ instead.
def uniq_value
distinct_value
end

# Compares two relations for equality.
def ==(other)
case other
Expand Down
4 changes: 2 additions & 2 deletions activerecord/lib/active_record/relation/calculations.rb
Expand Up @@ -198,8 +198,8 @@ def has_include?(column_name)
def perform_calculation(operation, column_name, options = {})
operation = operation.to_s.downcase

# If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count)
distinct = options[:distinct] || self.uniq_value
# If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
distinct = options[:distinct] || self.distinct_value

if operation == "count"
column_name ||= (select_for_count || :all)
Expand Down
20 changes: 11 additions & 9 deletions activerecord/lib/active_record/relation/query_methods.rb
Expand Up @@ -710,20 +710,22 @@ def from!(value, subquery_name = nil) # :nodoc:
# User.select(:name)
# # => Might return two records with the same name
#
# User.select(:name).uniq
# # => Returns 1 record per unique name
# User.select(:name).distinct
# # => Returns 1 record per distinct name
#
# User.select(:name).uniq.uniq(false)
# User.select(:name).distinct.distinct(false)
# # => You can also remove the uniqueness
def uniq(value = true)
spawn.uniq!(value)
def distinct(value = true)
spawn.distinct!(value)
end
alias uniq distinct

# Like #uniq, but modifies relation in place.
def uniq!(value = true) # :nodoc:
self.uniq_value = value
# Like #distinct, but modifies relation in place.
def distinct!(value = true) # :nodoc:
self.distinct_value = value
self
end
alias uniq! distinct!

# Used to extend a scope with additional methods, either through
# a module or through a block provided.
Expand Down Expand Up @@ -814,7 +816,7 @@ def build_arel

build_select(arel, select_values.uniq)

arel.distinct(uniq_value)
arel.distinct(distinct_value)
arel.from(build_from) if from_value
arel.lock(lock_value) if lock_value

Expand Down
Expand Up @@ -316,7 +316,7 @@ def test_uniq_after_the_fact
dev.projects << projects(:active_record)

assert_equal 3, dev.projects.size
assert_equal 1, dev.projects.uniq.size
assert_equal 1, dev.projects.distinct.size
end

def test_uniq_before_the_fact
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/cases/associations/join_model_test.rb
Expand Up @@ -397,14 +397,14 @@ def test_has_many_through_polymorphic_has_one
end

def test_has_many_through_polymorphic_has_many
assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id }
assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id }
end

def test_include_has_many_through_polymorphic_has_many
author = Author.includes(:taggings).find authors(:david).id
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
end
end

Expand Down
Expand Up @@ -410,7 +410,7 @@ def test_nested_has_many_through_with_a_table_referenced_multiple_times

# Mary and Bob both have posts in misc, but they are the only ones.
authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id)
assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)

# Check the polymorphism of taggings is being observed correctly (in both joins)
authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/cases/base_test.rb
Expand Up @@ -1499,6 +1499,12 @@ def test_uniq_delegates_to_scoped
assert_equal scope, Bird.uniq
end

def test_distinct_delegates_to_scoped
scope = stub
Bird.stubs(:all).returns(mock(:distinct => scope))
assert_equal scope, Bird.distinct
end

def test_table_name_with_2_abstract_subclasses
assert_equal "photos", Photo.table_name
end
Expand Down
3 changes: 2 additions & 1 deletion activerecord/test/cases/calculations_test.rb
Expand Up @@ -341,7 +341,8 @@ def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end

def test_count_with_uniq
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
assert_equal 4, Account.select(:credit_limit).uniq.count
end

Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/finder_test.rb
Expand Up @@ -82,7 +82,7 @@ def test_exists_with_nil_arg

# ensures +exists?+ runs valid SQL by excluding order value
def test_exists_with_order
assert Topic.order(:id).uniq.exists?
assert Topic.order(:id).distinct.exists?
end

def test_exists_with_includes_limit_and_empty_result
Expand Down
12 changes: 12 additions & 0 deletions activerecord/test/cases/relation_test.rb
Expand Up @@ -278,5 +278,17 @@ def relation
assert_equal [NullRelation], relation.extending_values
assert relation.is_a?(NullRelation)
end

test "distinct!" do
relation.distinct! :foo
assert_equal :foo, relation.distinct_value
assert_equal :foo, relation.uniq_value # deprecated access
end

test "uniq! was replaced by distinct!" do
relation.uniq! :foo
assert_equal :foo, relation.distinct_value
assert_equal :foo, relation.uniq_value # deprecated access
end
end
end
6 changes: 5 additions & 1 deletion activerecord/test/cases/relations_test.rb
Expand Up @@ -492,6 +492,7 @@ def test_dynamic_find_by_attributes
expected_taggings = taggings(:welcome_general, :thinking_general)

assert_no_queries do
assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
end

Expand Down Expand Up @@ -1269,19 +1270,22 @@ def test_update_all_with_joins_and_offset_and_order
assert_equal posts(:welcome), comments(:greetings).post
end

def test_uniq
def test_distinct
tag1 = Tag.create(:name => 'Foo')
tag2 = Tag.create(:name => 'Foo')

query = Tag.select(:name).where(:id => [tag1.id, tag2.id])

assert_equal ['Foo', 'Foo'], query.map(&:name)
assert_sql(/DISTINCT/) do
assert_equal ['Foo'], query.distinct.map(&:name)
assert_equal ['Foo'], query.uniq.map(&:name)
end
assert_sql(/DISTINCT/) do
assert_equal ['Foo'], query.distinct(true).map(&:name)
assert_equal ['Foo'], query.uniq(true).map(&:name)
end
assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name)
assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
end

Expand Down
8 changes: 4 additions & 4 deletions activerecord/test/models/author.rb
Expand Up @@ -30,8 +30,8 @@ class Author < ActiveRecord::Base
has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments
has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments
has_many :funky_comments, :through => :posts, :source => :comments
has_many :ordered_uniq_comments, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments
has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :through => :posts, :source => :comments
has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments
has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments
has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments

has_many :special_posts
Expand Down Expand Up @@ -78,7 +78,7 @@ class Author < ActiveRecord::Base
has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category'

has_many :categorized_posts, :through => :categorizations, :source => :post
has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post
has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post

has_many :nothings, :through => :kateggorisatons, :class_name => 'Category'

Expand All @@ -91,7 +91,7 @@ class Author < ActiveRecord::Base
has_many :post_categories, :through => :posts, :source => :categories
has_many :tagging_tags, :through => :taggings, :source => :tag

has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts
has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts
has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags

has_many :tags_with_primary_key, :through => :posts
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/models/book.rb
Expand Up @@ -2,7 +2,7 @@ class Book < ActiveRecord::Base
has_many :authors

has_many :citations, :foreign_key => 'book1_id'
has_many :references, -> { uniq }, :through => :citations, :source => :reference_of
has_many :references, -> { distinct }, :through => :citations, :source => :reference_of

has_many :subscriptions
has_many :subscribers, :through => :subscriptions
Expand Down
3 changes: 1 addition & 2 deletions activerecord/test/models/liquid.rb
@@ -1,5 +1,4 @@
class Liquid < ActiveRecord::Base
self.table_name = :liquid
has_many :molecules, -> { uniq }
has_many :molecules, -> { distinct }
end

8 changes: 4 additions & 4 deletions activerecord/test/models/project.rb
@@ -1,11 +1,11 @@
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' }
has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' }
has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer"
has_and_belongs_to_many :selected_developers, -> { uniq.select "developers.*" }, :class_name => "Developer"
has_and_belongs_to_many :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer"
has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer'
has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer"
has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer"

ActiveSupport::Deprecation.silence do
Expand Down
11 changes: 6 additions & 5 deletions guides/source/active_record_querying.md
Expand Up @@ -76,6 +76,7 @@ The methods are:
* `reorder`
* `reverse_order`
* `select`
* `distinct`
* `uniq`
* `where`

Expand Down Expand Up @@ -580,10 +581,10 @@ ActiveModel::MissingAttributeError: missing attribute: <attribute>

Where `<attribute>` is the attribute you asked for. The `id` method will not raise the `ActiveRecord::MissingAttributeError`, so just be careful when working with associations because they need the `id` method to function properly.

If you would like to only grab a single record per unique value in a certain field, you can use `uniq`:
If you would like to only grab a single record per unique value in a certain field, you can use `distinct`:

```ruby
Client.select(:name).uniq
Client.select(:name).distinct
```

This would generate SQL like:
Expand All @@ -595,10 +596,10 @@ SELECT DISTINCT name FROM clients
You can also remove the uniqueness constraint:

```ruby
query = Client.select(:name).uniq
query = Client.select(:name).distinct
# => Returns unique names

query.uniq(false)
query.distinct(false)
# => Returns all names, even if there are duplicates
```

Expand Down Expand Up @@ -1438,7 +1439,7 @@ Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]
Client.uniq.pluck(:role)
Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']
Expand Down

0 comments on commit a1bb6c8

Please sign in to comment.