diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 20d4429a06..3730dfac34 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,16 @@ +* Support passing record to uniqueness validator `:conditions` callable: + + ```ruby + class Article < ApplicationRecord + validates_uniqueness_of :title, conditions: ->(article) { + published_at = article.published_at + where(published_at: published_at.beginning_of_year..published_at.end_of_year) + } + end + ``` + + *Eliot Sykes* + * `BatchEnumerator#update_all` and `BatchEnumerator#delete_all` now return the total number of rows affected, just like their non-batched counterparts. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index ce5c9e4f31..af598ac5f4 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -29,7 +29,16 @@ module ActiveRecord end end relation = scope_relation(record, relation) - relation = relation.merge(options[:conditions]) if options[:conditions] + + if options[:conditions] + conditions = options[:conditions] + + relation = if conditions.arity.zero? + relation.instance_exec(&conditions) + else + relation.instance_exec(record, &conditions) + end + end if relation.exists? error_options = options.except(:case_sensitive, :scope, :conditions) @@ -126,6 +135,17 @@ module ActiveRecord # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') } # end # + # To build conditions based on the record's state, define the conditions + # callable with a parameter, which will be the record itself. This + # example validates the title is unique for the year of publication: + # + # class Article < ActiveRecord::Base + # validates_uniqueness_of :title, conditions: ->(article) { + # published_at = article.published_at + # where(published_at: published_at.beginning_of_year..published_at.end_of_year) + # } + # end + # # When the record is created, a check is performed to make sure that no # record exists in the database with the given value for the specified # attribute (that maps to a column). When the record is updated, diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 901e59f9ad..5036960c60 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -522,6 +522,23 @@ class UniquenessValidationTest < ActiveRecord::TestCase } end + def test_validate_uniqueness_with_conditions_with_record_arg + Topic.validates_uniqueness_of :title, conditions: ->(record) { + where(written_on: record.written_on.beginning_of_day..record.written_on.end_of_day) + } + + today_midday = Time.current.midday + + todays_topic = Topic.new(title: "Highlights of the Day", written_on: today_midday) + assert todays_topic.save, "1st topic written today with this title should save" + + todays_topic_duplicate = Topic.new(title: "Highlights of the Day", written_on: today_midday + 1.minute) + assert todays_topic_duplicate.invalid?, "2nd topic written today with this title should be invalid" + + tomorrows_topic = Topic.new(title: "Highlights of the Day", written_on: today_midday + 1.day) + assert tomorrows_topic.valid?, "1st topic written tomorrow with this title should be valid" + end + def test_validate_uniqueness_on_existing_relation event = Event.create assert_predicate TopicWithUniqEvent.create(event: event), :valid?