Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added strict_default_collation_match as a config option #1034

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,12 @@ If you would like to have an exact match covering special characters with MySql:
ActsAsTaggableOn.force_binary_collation = true
```

If you would like to not use `LOWER` for comparison queries and do not need to use `strict_case_match` or `force_binary_collation`.

```ruby
ActsAsTaggableOn.strict_default_collation_match = true
```

If you would like to specify table names:

```ruby
Expand Down
7 changes: 6 additions & 1 deletion lib/acts-as-taggable-on.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class Configuration
:remove_unused_tags, :default_parser,
:tags_counter, :tags_table,
:taggings_table
attr_reader :delimiter, :strict_case_match
attr_reader :delimiter, :strict_case_match, :strict_default_collation_match

def initialize
@delimiter = ','
Expand All @@ -79,12 +79,17 @@ def initialize
@force_binary_collation = false
@tags_table = :tags
@taggings_table = :taggings
@strict_default_collation_match = false
end

def strict_case_match=(force_cs)
@strict_case_match = force_cs unless @force_binary_collation
end

def strict_default_collation_match=(force_cs)
@strict_default_collation_match = force_cs unless @strict_case_match
end

def delimiter=(string)
ActiveRecord::Base.logger.warn <<WARNING
ActsAsTaggableOn.delimiter is deprecated \
Expand Down
8 changes: 6 additions & 2 deletions lib/acts_as_taggable_on/tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def validates_name_uniqueness?
def self.named(name)
if ActsAsTaggableOn.strict_case_match
where(["name = #{binary}?", as_8bit_ascii(name)])
elsif ActsAsTaggableOn.strict_default_collation_match
where(["name = ?", as_8bit_ascii(name)])
else
where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
end
Expand Down Expand Up @@ -58,7 +60,7 @@ def self.for_context(context)
### CLASS METHODS:

def self.find_or_create_with_like_by_name(name)
if ActsAsTaggableOn.strict_case_match
if ActsAsTaggableOn.strict_case_match || ActsAsTaggableOn.strict_default_collation_match
self.find_or_create_all_with_like_by_name([name]).first
else
named_like(name).first || create(name: name)
Expand Down Expand Up @@ -108,7 +110,7 @@ class << self
private

def comparable_name(str)
if ActsAsTaggableOn.strict_case_match
if ActsAsTaggableOn.strict_case_match || ActsAsTaggableOn.strict_default_collation_match
str
else
unicode_downcase(str.to_s)
Expand All @@ -130,6 +132,8 @@ def unicode_downcase(string)
def sanitize_sql_for_named_any(tag)
if ActsAsTaggableOn.strict_case_match
sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
elsif ActsAsTaggableOn.strict_default_collation_match
sanitize_sql(["name = ?", as_8bit_ascii(tag)])
else
sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def tagging_arel_table

def tag_match_type(tag)
matches_attribute = tag_arel_table[:name]
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match || ActsAsTaggableOn.strict_default_collation_match

if options[:wild].present?
matches_attribute.matches("%#{escaped_tag(tag)}%", "!", ActsAsTaggableOn.strict_case_match)
Expand All @@ -37,7 +37,7 @@ def tag_match_type(tag)

def tags_match_type
matches_attribute = tag_arel_table[:name]
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match || ActsAsTaggableOn.strict_default_collation_match

if options[:wild].present?
matches_attribute.matches_any(tag_list.map{|tag| "%#{escaped_tag(tag)}%"}, "!", ActsAsTaggableOn.strict_case_match)
Expand All @@ -47,7 +47,7 @@ def tags_match_type
end

def escaped_tag(tag)
tag = tag.downcase unless ActsAsTaggableOn.strict_case_match
tag = tag.downcase unless ActsAsTaggableOn.strict_case_match || ActsAsTaggableOn.strict_default_collation_match
ActsAsTaggableOn::Utils.escape_like(tag)
end

Expand Down
59 changes: 59 additions & 0 deletions spec/acts_as_taggable_on/tag_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,65 @@
end
end

describe 'when using strict_default_collation_match' do
before do
ActsAsTaggableOn.strict_default_collation_match = true
@tag.name = 'awesome'
@tag.save!
end

after do
ActsAsTaggableOn.strict_default_collation_match = false
end

it 'should find by name' do
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('awesome')).to eq(@tag)
end

context 'case sensitive' do
if using_case_insensitive_collation?
include_context 'without unique index'
end

it 'should find by name case sensitively' do
expect {
ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('AWESOME')
}.to change(ActsAsTaggableOn::Tag, :count)

expect(ActsAsTaggableOn::Tag.last.name).to eq('AWESOME')
end
end

context 'case sensitive' do
if using_case_insensitive_collation?
include_context 'without unique index'
end

it 'should have a named_scope named(something) that matches exactly' do
uppercase_tag = ActsAsTaggableOn::Tag.create(name: 'Cool')
@tag.name = 'cool'
@tag.save!

expect(ActsAsTaggableOn::Tag.named('cool')).to include(@tag)
expect(ActsAsTaggableOn::Tag.named('cool')).to_not include(uppercase_tag)
end
end

it 'should not change encoding' do
name = "\u3042"
original_encoding = name.encoding
record = ActsAsTaggableOn::Tag.find_or_create_with_like_by_name(name)
record.reload
expect(record.name.encoding).to eq(original_encoding)
end

context 'named any with some special characters combinations', if: using_mysql? do
it 'should not raise an invalid encoding exception' do
expect{ActsAsTaggableOn::Tag.named_any(["holä", "hol'ä"])}.not_to raise_error
end
end
end

describe 'name uniqeness validation' do
let(:duplicate_tag) { ActsAsTaggableOn::Tag.new(name: 'ror') }

Expand Down