Skip to content

Commit 9def053

Browse files
committed
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/ #1399 #13465 gitlabhq/gitlabhq@c1dddf8 huginn/huginn#1330 (comment) 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).
1 parent cbedbde commit 9def053

File tree

6 files changed

+86
-12
lines changed

6 files changed

+86
-12
lines changed

activerecord/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Deprecate mismatched collation comparison for uniquness validator.
2+
3+
Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
4+
To continue case sensitive comparison on the case insensitive column,
5+
pass `case_sensitive: true` option explicitly to the uniqueness validator.
6+
7+
*Ryuta Kamizono*
8+
19
* Add `reselect` method. This is a short-hand for `unscope(:select).select(fields)`.
210

311
Fixes #27340.

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ def raw_connection
506506
@connection
507507
end
508508

509-
def default_uniqueness_comparison(attribute, value) # :nodoc:
509+
def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
510510
case_sensitive_comparison(attribute, value)
511511
end
512512

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,20 @@ def primary_keys(table_name) # :nodoc:
453453
SQL
454454
end
455455

456+
def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
457+
column = column_for_attribute(attribute)
458+
459+
if column.collation && !column.case_sensitive?
460+
ActiveSupport::Deprecation.warn(<<~MSG.squish)
461+
Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
462+
To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
463+
pass `case_sensitive: true` option explicitly to the uniqueness validator.
464+
MSG
465+
end
466+
467+
super
468+
end
469+
456470
def case_sensitive_comparison(attribute, value) # :nodoc:
457471
column = column_for_attribute(attribute)
458472

activerecord/lib/active_record/validations/uniqueness.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def build_relation(klass, attribute, value)
6363
if bind.nil?
6464
attr.eq(bind)
6565
elsif !options.key?(:case_sensitive)
66-
klass.connection.default_uniqueness_comparison(attr, bind)
66+
klass.connection.default_uniqueness_comparison(attr, bind, klass)
6767
elsif options[:case_sensitive]
6868
klass.connection.case_sensitive_comparison(attr, bind)
6969
else

activerecord/test/cases/validations/uniqueness_validation_test.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,51 @@ def test_validate_case_insensitive_uniqueness_with_special_sql_like_chars
314314
assert t3.save, "Should save t3 as unique"
315315
end
316316

317+
if current_adapter?(:Mysql2Adapter)
318+
def test_deprecate_validate_uniqueness_mismatched_collation
319+
Topic.validates_uniqueness_of(:author_email_address)
320+
321+
topic1 = Topic.new(author_email_address: "david@loudthinking.com")
322+
topic2 = Topic.new(author_email_address: "David@loudthinking.com")
323+
324+
assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
325+
326+
assert_deprecated do
327+
assert_not topic1.valid?
328+
assert_not topic1.save
329+
assert topic2.valid?
330+
assert topic2.save
331+
end
332+
333+
assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
334+
assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
335+
end
336+
end
337+
338+
def test_validate_case_sensitive_uniqueness_by_default
339+
Topic.validates_uniqueness_of(:author_email_address)
340+
341+
topic1 = Topic.new(author_email_address: "david@loudthinking.com")
342+
topic2 = Topic.new(author_email_address: "David@loudthinking.com")
343+
344+
assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
345+
346+
ActiveSupport::Deprecation.silence do
347+
assert_not topic1.valid?
348+
assert_not topic1.save
349+
assert topic2.valid?
350+
assert topic2.save
351+
end
352+
353+
if current_adapter?(:Mysql2Adapter)
354+
assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
355+
assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
356+
else
357+
assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
358+
assert_equal 1, Topic.where(author_email_address: "David@loudthinking.com").count
359+
end
360+
end
361+
317362
def test_validate_case_sensitive_uniqueness
318363
Topic.validates_uniqueness_of(:title, case_sensitive: true, allow_nil: true)
319364

activerecord/test/schema/schema.rb

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
# #
99
# ------------------------------------------------------------------- #
1010

11+
case_sensitive_options =
12+
if current_adapter?(:Mysql2Adapter)
13+
{ collation: "utf8mb4_bin" }
14+
else
15+
{}
16+
end
17+
1118
create_table :accounts, force: true do |t|
1219
t.references :firm, index: false
1320
t.string :firm_name
@@ -266,7 +273,7 @@
266273
end
267274

268275
create_table :dashboards, force: true, id: false do |t|
269-
t.string :dashboard_id
276+
t.string :dashboard_id, **case_sensitive_options
270277
t.string :name
271278
end
272279

@@ -330,15 +337,15 @@
330337
end
331338

332339
create_table :essays, force: true do |t|
333-
t.string :name
340+
t.string :name, **case_sensitive_options
334341
t.string :writer_id
335342
t.string :writer_type
336343
t.string :category_id
337344
t.string :author_id
338345
end
339346

340347
create_table :events, force: true do |t|
341-
t.string :title, limit: 5
348+
t.string :title, limit: 5, **case_sensitive_options
342349
end
343350

344351
create_table :eyes, force: true do |t|
@@ -380,16 +387,16 @@
380387
end
381388

382389
create_table :guids, force: true do |t|
383-
t.column :key, :string
390+
t.column :key, :string, **case_sensitive_options
384391
end
385392

386393
create_table :guitars, force: true do |t|
387394
t.string :color
388395
end
389396

390397
create_table :inept_wizards, force: true do |t|
391-
t.column :name, :string, null: false
392-
t.column :city, :string, null: false
398+
t.column :name, :string, null: false, **case_sensitive_options
399+
t.column :city, :string, null: false, **case_sensitive_options
393400
t.column :type, :string
394401
end
395402

@@ -876,8 +883,8 @@
876883
end
877884

878885
create_table :topics, force: true do |t|
879-
t.string :title, limit: 250
880-
t.string :author_name
886+
t.string :title, limit: 250, **case_sensitive_options
887+
t.string :author_name, **case_sensitive_options
881888
t.string :author_email_address
882889
if subsecond_precision_supported?
883890
t.datetime :written_on, precision: 6
@@ -889,10 +896,10 @@
889896
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
890897
# Oracle SELECT WHERE clause which causes many unit test failures
891898
if current_adapter?(:OracleAdapter)
892-
t.string :content, limit: 4000
899+
t.string :content, limit: 4000, **case_sensitive_options
893900
t.string :important, limit: 4000
894901
else
895-
t.text :content
902+
t.text :content, **case_sensitive_options
896903
t.text :important
897904
end
898905
t.boolean :approved, default: true

0 commit comments

Comments
 (0)