Skip to content


Subversion checkout URL

You can clone with
Download ZIP


Rollback to LOWER comparison instead of (I)LIKE because of bug and poor Postgres performance #339

merged 1 commit into from

8 participants


Hi, Ben.

I suggest to revert changes with LIKE back to LOWER, because this solution doesn't add any benefits and introduce subtle bug.

1) (I)LIKE is used without any quotiong. This leads to login/email collision. For example, user1 has email: and user2 has email: If user1 is registered before user2, user2 isn't able to login. Because query:

SELECT  "users".* FROM "users"  WHERE ("users".email ILIKE '') LIMIT 1;

allways returns user1 record. Take a look on issue 321, they already obtain this email collision: binarylogic/authlogic/issues/#321

2) For MySQL case-insensitive is usualy default, because Rails creates database with collation "utf8_general_ci", and with such collation search by string fields is case insensitive. So, MySQL doesn't benefit from LIKE, because it doesn't make sense for it to use such option. Plane find_by_#{field} does all in case-insensitive manner.

3) For Postgres ILIKE only brings new issue instead of solving something.
First of all, ILIKE can't use indexes. You can clearly see this via EXPLAIN:

EXPLAIN SELECT  "users".* FROM "users"  WHERE ("users".email ILIKE '') LIMIT 1;
                            QUERY PLAN                             
 Limit  (cost=0.00..846.95 rows=1 width=1903)
   ->  Seq Scan on users  (cost=0.00..32184.19 rows=38 width=1903)
         Filter: ((email)::text ~~* ''::text)
(3 rows)

What is worse, now I can't use Postgres ability to create index on function. For previous version of Authlogic I've created index on expression: LOWER(email) and it provides me with required functionality.

explain SELECT  "users".* FROM "users"  WHERE (LOWER("users".email) = '') LIMIT 1;
                                          QUERY PLAN                                          
 Limit  (cost=0.00..8.48 rows=1 width=1903)
   ->  Index Scan using index_users_lower_email on users  (cost=0.00..8.48 rows=1 width=1903)
         Index Cond: (lower((email)::text) = ''::text)

After update to the latest version of AuthLogic I lost such ability.

Please, apply this pull-request to handle all caveats.


+1. The ILIKE quoting issue is fairly serious, and switching back to LOWER seems like a good way to both fix it and improve performance on Postgres at the same time.

It looks like the original switch to LIKE was done for "performance" reasons on MySQL, but wasn't thought through completely as it actually degraded performance on all versions of Postgres.

(for reference here's the original LIKE commit)



+1 this is a much better solution.

@binarylogic binarylogic merged commit d8c61db into binarylogic:master

FWIW this change destroys performance on MySQL, since the use of LOWER prevents MySQL from being able to employ indices at all.


Well, this caused a production outage for us. When you use LOWER on a column in MySQL, you can't index that constraint. Every user lookup turned into a full table scan.

The solution is probably to detect Postgres and use LOWER there, for mysql and others use LIKE.

@mperham, you don't need LOWER or ILIKE anyway. Please, read this article:

ILIKE isn't needed for MySQL at all. Correct way - is to disable case_sensitive search and it will use plain equal-comparison which is by default case-insensitive in MySQL(if you use utf8_general_ci).
This should fix an issue:

class User < ActiveRecord::Base
  # ...
  acts_as_authentic do |c|
    c.validates_uniqueness_of_email_field_options case_sensitive: true
    c.validates_uniqueness_of_login_field_options case_sensitive: true
    # ... other settings
  # ...

@cris How was I supposed to know to make that change when upgrading to 3.2? There's no changelog or any sort of upgrade notes.


@mperham just opened a pull request that I think might fix it for you. It follows Rails implementation and only uses LOWER if it's a case-sensitive column. Thoughts?



Also, good call about the changelog. Perhaps we should introduce one? :\

@tiegz I just added the config that @cris mentioned and it solved the performance problem. It does scare me that the default config has this terrible performance problem and all customers on mysql are expected to find and change this config. It would be nice to have it just work.

@tiegz and your PR looks great to me. :clap:

@pixeltrix pixeltrix referenced this pull request from a commit in alphagov/e-petitions
@pixeltrix pixeltrix Don't enforce case-sensitivity for admin usernames
As of Rails 4.2 enforcing case-sensitivity for usernames is broken in
Authlogic due to the commit rails/rails@a38e957. The app change was for
security reasons since at the time Authlogic was using LIKE in the
query generation. However this was fixed in binarylogic/authlogic#339.

- binarylogic/authlogic#444
- binarylogic/authlogic#453

We should leave it as case-insensitive as this is better UX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 26, 2012
  1. @cris

    Rollback to LOWER comparison instead of (I)LIKE because of bug and in…

    cris committed
    …ability to use index with ILIKE for Postgres
This page is out of date. Refresh to see the latest.
Showing with 2 additions and 3 deletions.
  1. +2 −3 lib/authlogic/acts_as_authentic/login.rb
5 lib/authlogic/acts_as_authentic/login.rb
@@ -94,7 +94,7 @@ def merge_validates_uniqueness_of_login_field_options(options = {})
# manner that they handle that. If you are using the login field and set false for the :case_sensitive option in
# validates_uniqueness_of_login_field_options this method will modify the query to look something like:
- # where("#{quoted_table_name}.#{field} LIKE ?", login).first
+ # where("LOWER(#{quoted_table_name}.#{login_field}) = ?", login.downcase).first
# If you don't specify this it calls the good old find_by_* method:
@@ -118,8 +118,7 @@ def find_with_case(field, value, sensitivity = true)
if sensitivity
send("find_by_#{field}", value)
- like_word = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
- where("#{quoted_table_name}.#{field} #{like_word} ?", value.mb_chars).first
+ where("LOWER(#{quoted_table_name}.#{field}) = ?", value.mb_chars.downcase).first
Something went wrong with that request. Please try again.