Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

validates_uniqueness_of should honor the collation #13465

Open
wants to merge 1 commit into from

3 participants

@shugo

Currently, if the case_sensitive option of validates_uniqueness_of is omitted, true is used by default.
However, the default collation of MySQL for rake db:create:all in Rails is utf8_unicode_ci, which means that you need to specify case_sensitive: false for each validates_uniqueness_of, except when the collation is explicitly set to *_bin. Otherwise, a DB-level unique constraint violation may occur even if the AR-level validation succeeded.

In MySQL each column has its own collation, and the preferred case sensitivity can be guessed from the value of the collation. Why not use the guessed value as the default?

@vipulnsward vipulnsward commented on the diff
...ecord/lib/active_record/connection_adapters/column.rb
@@ -120,6 +120,10 @@ def extract_default(default)
type_cast(default)
end
+ def case_sensitive?
+ true
+ end
+

Is it case-sensitive by default for all adapters?

@shugo
shugo added a note

Yes, it is. An adapter should override it, if necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 24, 2013
  1. @shugo

    Check the case sensitivity of the collation when the case_sensitive o…

    shugo authored
    …ption of validates_uniqueness_of is omitted.
This page is out of date. Refresh to see the latest.
View
4 activerecord/lib/active_record/connection_adapters/column.rb
@@ -120,6 +120,10 @@ def extract_default(default)
type_cast(default)
end
+ def case_sensitive?
+ true
+ end
+

Is it case-sensitive by default for all adapters?

@shugo
shugo added a note

Yes, it is. An adapter should override it, if necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
class << self
# Used to convert from BLOBs to Strings
def binary_to_string(value)
View
9 activerecord/lib/active_record/validations/uniqueness.rb
@@ -6,7 +6,7 @@ def initialize(options)
raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
end
- super({ case_sensitive: true }.merge!(options))
+ super(options)
@klass = options[:class]
end
@@ -63,7 +63,12 @@ def build_relation(klass, table, attribute, value) #:nodoc:
value = klass.connection.type_cast(value, column)
value = value.to_s[0, column.limit] if value && column.limit && column.text?
- if !options[:case_sensitive] && value && column.text?
+ if options.key?(:case_sensitive)
+ case_sensitive = options[:case_sensitive]
+ else
+ case_sensitive = column.case_sensitive?
+ end
+ if !case_sensitive && value && column.text?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
View
35 activerecord/test/cases/adapters/mysql/case_sensitivity_default_test.rb
@@ -0,0 +1,35 @@
+require "cases/helper"
+require 'models/person'
+
+class MysqlCaseSensitivityDefaultTest < ActiveRecord::TestCase
+ class CollationTest < ActiveRecord::Base
+ validates_uniqueness_of :string_cs_column
+ validates_uniqueness_of :string_ci_column
+ end
+
+ def teardown
+ CollationTest.delete_all
+ end
+
+ def test_default_comparison_for_ci_column
+ CollationTest.create!(:string_ci_column => 'A',
+ :string_cs_column => 'b')
+ invalid = CollationTest.new(:string_ci_column => 'a',
+ :string_cs_column => 'c')
+ queries = assert_sql { invalid.save }
+ assert_equal(["has already been taken"], invalid.errors[:string_ci_column])
+ ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
+ assert_no_match(/binary/i, ci_uniqueness_query)
+ end
+
+ def test_default_comparison_for_cs_column
+ CollationTest.create!(:string_cs_column => 'A',
+ :string_ci_column => 'b')
+ invalid = CollationTest.new(:string_cs_column => 'a',
+ :string_ci_column => 'c')
+ queries = assert_sql { invalid.save }
+ assert_equal([], invalid.errors[:string_cs_column])
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)}
+ assert_no_match(/lower/i, cs_uniqueness_query)
+ end
+end
View
35 activerecord/test/cases/adapters/mysql2/case_sensitivity_default_test.rb
@@ -0,0 +1,35 @@
+require "cases/helper"
+require 'models/person'
+
+class Mysql2CaseSensitivityDefaultTest < ActiveRecord::TestCase
+ class CollationTest < ActiveRecord::Base
+ validates_uniqueness_of :string_cs_column
+ validates_uniqueness_of :string_ci_column
+ end
+
+ def teardown
+ CollationTest.delete_all
+ end
+
+ def test_default_comparison_for_ci_column
+ CollationTest.create!(:string_ci_column => 'A',
+ :string_cs_column => 'b')
+ invalid = CollationTest.new(:string_ci_column => 'a',
+ :string_cs_column => 'c')
+ queries = assert_sql { invalid.save }
+ assert_equal(["has already been taken"], invalid.errors[:string_ci_column])
+ ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
+ assert_no_match(/binary/i, ci_uniqueness_query)
+ end
+
+ def test_default_comparison_for_cs_column
+ CollationTest.create!(:string_cs_column => 'A',
+ :string_ci_column => 'b')
+ invalid = CollationTest.new(:string_cs_column => 'a',
+ :string_ci_column => 'c')
+ queries = assert_sql { invalid.save }
+ assert_equal([], invalid.errors[:string_cs_column])
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)}
+ assert_no_match(/lower/i, cs_uniqueness_query)
+ end
+end
Something went wrong with that request. Please try again.