From 1190d0ab3dc7a3025bf55b666f34d1a0b51a8d89 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 7 Oct 2015 14:03:18 +0200 Subject: [PATCH] Added concern for case-insensitive WHERE queries On PostgreSQL these queries use LOWER(...) to compare columns and values. For MySQL a regular WHERE is performed as MySQL is already case-insensitive. --- app/models/concerns/case_sensitivity.rb | 28 +++ spec/models/concerns/case_sensitivity_spec.rb | 176 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 app/models/concerns/case_sensitivity.rb create mode 100644 spec/models/concerns/case_sensitivity_spec.rb diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb new file mode 100644 index 00000000000..49d350e092b --- /dev/null +++ b/app/models/concerns/case_sensitivity.rb @@ -0,0 +1,28 @@ +# Concern for querying columns with specific case sensitivity handling. +module CaseSensitivity + extend ActiveSupport::Concern + + module ClassMethods + # Queries the given columns regardless of the casing used. + # + # Unlike other ActiveRecord methods this method only operates on a Hash. + def case_insensitive_where(params) + criteria = self + cast_lower = Gitlab::Database.postgresql? + + params.each do |key, value| + column = ActiveRecord::Base.connection.quote_table_name(key) + + if cast_lower + condition = "LOWER(#{column}) = LOWER(:value)" + else + condition = "#{column} = :value" + end + + criteria = criteria.where(condition, value: value) + end + + criteria + end + end +end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb new file mode 100644 index 00000000000..8b9f50aada7 --- /dev/null +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -0,0 +1,176 @@ +require 'spec_helper' + +describe CaseSensitivity do + describe '.case_insensitive_where' do + let(:connection) { ActiveRecord::Base.connection } + let(:model) { Class.new { include CaseSensitivity } } + + describe 'using PostgreSQL' do + describe 'with a single column/value pair' do + it 'returns the criteria for a column and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('"foo"') + + expect(model).to receive(:where). + with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where(foo: 'bar')).to eq(criteria) + end + + it 'returns the criteria for a column with a table, and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('"foo"."bar"') + + expect(model).to receive(:where). + with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where('foo.bar': 'bar')).to eq(criteria) + end + end + + describe 'with multiple column/value pairs' do + it 'returns the criteria for a column and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('"foo"') + + expect(connection).to receive(:quote_table_name). + with(:bar). + and_return('"bar"') + + expect(model).to receive(:where). + with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where(foo: 'bar', bar: 'baz') + + expect(got).to eq(final) + end + + it 'returns the criteria for a column with a table, and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('"foo"."bar"') + + expect(connection).to receive(:quote_table_name). + with(:'foo.baz'). + and_return('"foo"."baz"') + + expect(model).to receive(:where). + with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where('foo.bar': 'bar', 'foo.baz': 'baz') + + expect(got).to eq(final) + end + end + end + + describe 'using MySQL' do + describe 'with a single column/value pair' do + it 'returns the criteria for a column and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('`foo`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`) = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where(foo: 'bar')).to eq(criteria) + end + + it 'returns the criteria for a column with a table, and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('`foo`.`bar`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`.`bar`) = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.case_insensitive_where('foo.bar': 'bar')).to eq(criteria) + end + end + + describe 'with multiple column/value pairs' do + it 'returns the criteria for a column and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('`foo`') + + expect(connection).to receive(:quote_table_name). + with(:bar). + and_return('`bar`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`) = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER(`bar`) = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where(foo: 'bar', bar: 'baz') + + expect(got).to eq(final) + end + + it 'returns the criteria for a column with a table, and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('`foo`.`bar`') + + expect(connection).to receive(:quote_table_name). + with(:'foo.baz'). + and_return('`foo`.`baz`') + + expect(model).to receive(:where). + with(%q{LOWER(`foo`.`bar`) = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER(`foo`.`baz`) = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.case_insensitive_where('foo.bar': 'bar', 'foo.baz': 'baz') + + expect(got).to eq(final) + end + end + end + end +end