mirror of https://github.com/rails/rails
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:
parent
cbedbdef07
commit
9def05385f
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue