Only use LOWER for mysql case insensitive uniqueness check when column has a case sensitive collation.

This commit is contained in:
Joseph Palermo 2011-10-09 04:38:36 -07:00 committed by Jeremy Kemper
parent 8f8fd4f3aa
commit c90e5ce779
10 changed files with 139 additions and 12 deletions

@ -1,10 +1,18 @@
*Rails 3.1.1 (unreleased)*
*Rails 3.2.0 (unreleased)*
* MySQL: case-insensitive uniqueness validation avoids calling LOWER when
the column already uses a case-insensitive collation. Fixes #561.
[Joseph Palermo]
* Transactional fixtures enlist all active database connections. You can test
models on different connections without disabling transactional fixtures.
[Jeremy Kemper]
*Rails 3.1.1 (October 7, 2011)*
* Add deprecation for the preload_associations method. Fixes #3022.
[Jon Leighton]
@ -65,6 +73,7 @@ a URI that specifies the connection configuration. For example:
[Prem Sichanugrist]
*Rails 3.1.0 (August 30, 2011)*
* Add a proxy_association method to association proxies, which can be called by association

@ -238,6 +238,10 @@ def case_sensitive_modifier(node)
node
end
def case_insensitive_comparison(table, attribute, column, value)
table[attribute].lower.eq(table.lower(value))
end
def current_savepoint_name
"active_record_#{open_transactions}"
end

@ -4,6 +4,13 @@ module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
class Column < ConnectionAdapters::Column # :nodoc:
attr_reader :collation
def initialize(name, default, sql_type = nil, null = true, collation = nil)
super(name, default, sql_type, null)
@collation = collation
end
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
@ -28,6 +35,10 @@ def adapter
raise NotImplementedError
end
def case_sensitive?
collation && !collation.match(/_ci$/)
end
private
def simplified_type(field_type)
@ -157,8 +168,8 @@ def each_hash(result) # :nodoc:
end
# Overridden by the adapters to instantiate their specific Column type.
def new_column(field, default, type, null) # :nodoc:
Column.new(field, default, type, null)
def new_column(field, default, type, null, collation) # :nodoc:
Column.new(field, default, type, null, collation)
end
# Must return the Mysql error number from the exception, if the exception has an
@ -393,10 +404,10 @@ def indexes(table_name, name = nil) #:nodoc:
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation])
end
end
end
@ -501,6 +512,14 @@ def case_sensitive_modifier(node)
Arel::Nodes::Bin.new(node)
end
def case_insensitive_comparison(table, attribute, column, value)
if column.case_sensitive?
super
else
table[attribute].eq(value)
end
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end

@ -47,8 +47,8 @@ def each_hash(result) # :nodoc:
end
end
def new_column(field, default, type, null) # :nodoc:
Column.new(field, default, type, null)
def new_column(field, default, type, null, collation) # :nodoc:
Column.new(field, default, type, null, collation)
end
def error_number(exception)

@ -150,8 +150,8 @@ def each_hash(result) # :nodoc:
end
end
def new_column(field, default, type, null) # :nodoc:
Column.new(field, default, type, null)
def new_column(field, default, type, null, collation) # :nodoc:
Column.new(field, default, type, null, collation)
end
def error_number(exception) # :nodoc:

@ -57,8 +57,8 @@ def build_relation(klass, table, attribute, value) #:nodoc:
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text?
if !options[:case_sensitive] && value && column.text?
# will use SQL LOWER function before comparison
relation = table[attribute].lower.eq(table.lower(value))
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
value = klass.connection.case_sensitive_modifier(value)
relation = table[attribute].eq(value)

@ -0,0 +1,35 @@
require "cases/helper"
require 'models/person'
class MysqlCaseSensitivityTest < ActiveRecord::TestCase
class CollationTest < ActiveRecord::Base
validates_uniqueness_of :string_cs_column, :case_sensitive => false
validates_uniqueness_of :string_ci_column, :case_sensitive => false
end
def test_columns_include_collation_different_from_table
assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
end
def test_case_sensitive
assert !CollationTest.columns_hash['string_ci_column'].case_sensitive?
assert CollationTest.columns_hash['string_cs_column'].case_sensitive?
end
def test_case_insensitive_comparison_for_ci_column
CollationTest.create!(:string_ci_column => 'A')
invalid = CollationTest.new(:string_ci_column => 'a')
queries = assert_sql { invalid.save }
ci_uniqueness_query = queries.detect { |q| q.match /string_ci_column/ }
assert_no_match(/lower/i, ci_uniqueness_query)
end
def test_case_insensitive_comparison_for_cs_column
CollationTest.create!(:string_cs_column => 'A')
invalid = CollationTest.new(:string_cs_column => 'a')
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match /string_cs_column/ }
assert_match(/lower/i, cs_uniqueness_query)
end
end

@ -0,0 +1,38 @@
require "cases/helper"
require 'models/person'
class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
class CollationTest < ActiveRecord::Base
validates_uniqueness_of :string_cs_column, :case_sensitive => false
validates_uniqueness_of :string_ci_column, :case_sensitive => false
end
def test_columns_include_collation_different_from_table
assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
end
def test_case_sensitive
assert !CollationTest.columns_hash['string_ci_column'].case_sensitive?
assert CollationTest.columns_hash['string_cs_column'].case_sensitive?
end
def test_case_insensitive_comparison_for_ci_column
CollationTest.create!(:string_ci_column => 'A')
invalid = CollationTest.new(:string_ci_column => 'a')
queries = assert_sql { invalid.save }
ci_uniqueness_query = queries.detect { |q| q.match /string_ci_column/ }
assert_no_match(/lower/i, ci_uniqueness_query)
end
def test_case_insensitive_comparison_for_cs_column
CollationTest.create!(:string_cs_column => 'A')
invalid = CollationTest.new(:string_cs_column => 'a')
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match /string_cs_column/ }
assert_match(/lower/i, cs_uniqueness_query)
end
end

@ -21,4 +21,15 @@
END
SQL
end
ActiveRecord::Base.connection.execute <<-SQL
DROP TABLE IF EXISTS collation_tests;
SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE collation_tests (
string_cs_column VARCHAR(1) COLLATE utf8_bin,
string_ci_column VARCHAR(1) COLLATE utf8_general_ci
) CHARACTER SET utf8 COLLATE utf8_general_ci
SQL
end

@ -30,6 +30,17 @@
BEGIN
select * from topics limit 1;
END
SQL
ActiveRecord::Base.connection.execute <<-SQL
DROP TABLE IF EXISTS collation_tests;
SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE collation_tests (
string_cs_column VARCHAR(1) COLLATE utf8_bin,
string_ci_column VARCHAR(1) COLLATE utf8_general_ci
) CHARACTER SET utf8 COLLATE utf8_general_ci
SQL
end