Deprecate mismatched collation comparison for uniquness validator

In MySQL, the default collation is case insensitive. Since the
uniqueness validator enforces case sensitive comparison by default, it
frequently causes mismatched collation issues (performance, weird
behavior, etc) to MySQL users.

https://grosser.it/2009/12/11/validates_uniqness_of-mysql-slow/
https://github.com/rails/rails/issues/1399
https://github.com/rails/rails/pull/13465
c1dddf8c7d
https://github.com/huginn/huginn/pull/1330#discussion_r55152573

I'd like to deprecate the implicit default enforcing since I frequently
experienced the problems in code reviews.

Note that this change has no effect to sqlite3, postgresql, and
oracle-enhanced adapters which are implemented as case sensitive by
default, only affect to mysql2 adapter (I can take a work if sqlserver
adapter will support Rails 6.0).
This commit is contained in:
Ryuta Kamizono 2019-02-21 17:30:27 +09:00
parent cbedbdef07
commit 9def05385f
6 changed files with 86 additions and 12 deletions

View File

@ -1,3 +1,11 @@
* Deprecate mismatched collation comparison for uniquness validator.
Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
To continue case sensitive comparison on the case insensitive column,
pass `case_sensitive: true` option explicitly to the uniqueness validator.
*Ryuta Kamizono*
* Add `reselect` method. This is a short-hand for `unscope(:select).select(fields)`.
Fixes #27340.

View File

@ -506,7 +506,7 @@ module ActiveRecord
@connection
end
def default_uniqueness_comparison(attribute, value) # :nodoc:
def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
case_sensitive_comparison(attribute, value)
end

View File

@ -453,6 +453,20 @@ module ActiveRecord
SQL
end
def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
column = column_for_attribute(attribute)
if column.collation && !column.case_sensitive?
ActiveSupport::Deprecation.warn(<<~MSG.squish)
Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
pass `case_sensitive: true` option explicitly to the uniqueness validator.
MSG
end
super
end
def case_sensitive_comparison(attribute, value) # :nodoc:
column = column_for_attribute(attribute)

View File

@ -63,7 +63,7 @@ module ActiveRecord
if bind.nil?
attr.eq(bind)
elsif !options.key?(:case_sensitive)
klass.connection.default_uniqueness_comparison(attr, bind)
klass.connection.default_uniqueness_comparison(attr, bind, klass)
elsif options[:case_sensitive]
klass.connection.case_sensitive_comparison(attr, bind)
else

View File

@ -314,6 +314,51 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t3.save, "Should save t3 as unique"
end
if current_adapter?(:Mysql2Adapter)
def test_deprecate_validate_uniqueness_mismatched_collation
Topic.validates_uniqueness_of(:author_email_address)
topic1 = Topic.new(author_email_address: "david@loudthinking.com")
topic2 = Topic.new(author_email_address: "David@loudthinking.com")
assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
assert_deprecated do
assert_not topic1.valid?
assert_not topic1.save
assert topic2.valid?
assert topic2.save
end
assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
end
end
def test_validate_case_sensitive_uniqueness_by_default
Topic.validates_uniqueness_of(:author_email_address)
topic1 = Topic.new(author_email_address: "david@loudthinking.com")
topic2 = Topic.new(author_email_address: "David@loudthinking.com")
assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
ActiveSupport::Deprecation.silence do
assert_not topic1.valid?
assert_not topic1.save
assert topic2.valid?
assert topic2.save
end
if current_adapter?(:Mysql2Adapter)
assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
else
assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
assert_equal 1, Topic.where(author_email_address: "David@loudthinking.com").count
end
end
def test_validate_case_sensitive_uniqueness
Topic.validates_uniqueness_of(:title, case_sensitive: true, allow_nil: true)

View File

@ -8,6 +8,13 @@ ActiveRecord::Schema.define do
# #
# ------------------------------------------------------------------- #
case_sensitive_options =
if current_adapter?(:Mysql2Adapter)
{ collation: "utf8mb4_bin" }
else
{}
end
create_table :accounts, force: true do |t|
t.references :firm, index: false
t.string :firm_name
@ -266,7 +273,7 @@ ActiveRecord::Schema.define do
end
create_table :dashboards, force: true, id: false do |t|
t.string :dashboard_id
t.string :dashboard_id, **case_sensitive_options
t.string :name
end
@ -330,7 +337,7 @@ ActiveRecord::Schema.define do
end
create_table :essays, force: true do |t|
t.string :name
t.string :name, **case_sensitive_options
t.string :writer_id
t.string :writer_type
t.string :category_id
@ -338,7 +345,7 @@ ActiveRecord::Schema.define do
end
create_table :events, force: true do |t|
t.string :title, limit: 5
t.string :title, limit: 5, **case_sensitive_options
end
create_table :eyes, force: true do |t|
@ -380,7 +387,7 @@ ActiveRecord::Schema.define do
end
create_table :guids, force: true do |t|
t.column :key, :string
t.column :key, :string, **case_sensitive_options
end
create_table :guitars, force: true do |t|
@ -388,8 +395,8 @@ ActiveRecord::Schema.define do
end
create_table :inept_wizards, force: true do |t|
t.column :name, :string, null: false
t.column :city, :string, null: false
t.column :name, :string, null: false, **case_sensitive_options
t.column :city, :string, null: false, **case_sensitive_options
t.column :type, :string
end
@ -876,8 +883,8 @@ ActiveRecord::Schema.define do
end
create_table :topics, force: true do |t|
t.string :title, limit: 250
t.string :author_name
t.string :title, limit: 250, **case_sensitive_options
t.string :author_name, **case_sensitive_options
t.string :author_email_address
if subsecond_precision_supported?
t.datetime :written_on, precision: 6
@ -889,10 +896,10 @@ ActiveRecord::Schema.define do
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
t.string :content, limit: 4000
t.string :content, limit: 4000, **case_sensitive_options
t.string :important, limit: 4000
else
t.text :content
t.text :content, **case_sensitive_options
t.text :important
end
t.boolean :approved, default: true