Testing Predicate Query Generation in Ransack
This test suite comprehensively validates the predicate functionality in the Ransack search library, focusing on SQL query generation for various comparison operators and search conditions. The tests ensure proper handling of boolean values, null checks, string matching, and custom predicate definitions.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
activerecord-hackery/ransack
spec/ransack/predicate_spec.rb
require 'spec_helper'
module Ransack
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
describe Predicate do
before do
@s = Search.new(Person)
end
shared_examples 'wildcard escaping' do |method, regexp|
it 'automatically converts integers to strings' do
subject.parent_id_cont = 1
expect { subject.result }.to_not raise_error
end
it "escapes '%', '.', '_' and '\\\\' in value" do
subject.send(:"#{method}=", '%._\\')
expect(subject.result.to_sql).to match(regexp)
end
end
describe 'eq' do
it 'generates an equality condition for boolean true values' do
test_boolean_equality_for(true)
end
it 'generates an equality condition for boolean false values' do
test_boolean_equality_for(false)
end
it 'does not generate a condition for nil' do
@s.awesome_eq = nil
expect(@s.result.to_sql).not_to match /WHERE/
end
it 'generates a = condition with a huge integer value' do
val = 123456789012345678901
@s.salary_eq = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} = #{val}/
end
end
describe 'lteq' do
it 'generates a <= condition with an integer column' do
val = 1000
@s.salary_lteq = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} <= #{val}/
end
it 'generates a <= condition with a string column' do
val = '[email protected]'
@s.email_lteq = val
field = "#{quote_table_name("people")}.#{quote_column_name("email")}"
expect(@s.result.to_sql).to match /#{field} <= '#{val}'/
end
it 'does not generate a condition for nil' do
@s.salary_lteq = nil
expect(@s.result.to_sql).not_to match /WHERE/
end
it 'generates a <= condition with a huge integer value' do
val = 123456789012345678901
@s.salary_lteq = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} <= #{val}/
end
end
describe 'lt' do
it 'generates a < condition with an integer column' do
val = 2000
@s.salary_lt = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} < #{val}/
end
it 'generates a < condition with a string column' do
val = '[email protected]'
@s.email_lt = val
field = "#{quote_table_name("people")}.#{quote_column_name("email")}"
expect(@s.result.to_sql).to match /#{field} < '#{val}'/
end
it 'does not generate a condition for nil' do
@s.salary_lt = nil
expect(@s.result.to_sql).not_to match /WHERE/
end
it 'generates a = condition with a huge integer value' do
val = 123456789012345678901
@s.salary_lt = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} < #{val}/
end
end
describe 'gteq' do
it 'generates a >= condition with an integer column' do
val = 300
@s.salary_gteq = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} >= #{val}/
end
it 'generates a >= condition with a string column' do
val = '[email protected]'
@s.email_gteq = val
field = "#{quote_table_name("people")}.#{quote_column_name("email")}"
expect(@s.result.to_sql).to match /#{field} >= '#{val}'/
end
it 'does not generate a condition for nil' do
@s.salary_gteq = nil
expect(@s.result.to_sql).not_to match /WHERE/
end
it 'generates a >= condition with a huge integer value' do
val = 123456789012345678901
@s.salary_gteq = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} >= #{val}/
end
end
describe 'gt' do
it 'generates a > condition with an integer column' do
val = 400
@s.salary_gt = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} > #{val}/
end
it 'generates a > condition with a string column' do
val = '[email protected]'
@s.email_gt = val
field = "#{quote_table_name("people")}.#{quote_column_name("email")}"
expect(@s.result.to_sql).to match /#{field} > '#{val}'/
end
it 'does not generate a condition for nil' do
@s.salary_gt = nil
expect(@s.result.to_sql).not_to match /WHERE/
end
it 'generates a > condition with a huge integer value' do
val = 123456789012345678901
@s.salary_gt = val
field = "#{quote_table_name("people")}.#{quote_column_name("salary")}"
expect(@s.result.to_sql).to match /#{field} > #{val}/
end
end
describe 'cont' do
it_has_behavior 'wildcard escaping', :name_cont,
(if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
/"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
/`people`.`name` LIKE '%\\\\%.\\\\_\\\\\\\\%'/
else
/"people"."name" LIKE '%%._\\%'/
end) do
subject { @s }
end
it 'generates a LIKE query with value surrounded by %' do
@s.name_cont = 'ric'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE '%ric%'/
end
end
describe 'not_cont' do
it_has_behavior 'wildcard escaping', :name_not_cont,
(if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
/"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
/`people`.`name` NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
else
/"people"."name" NOT LIKE '%%._\\%'/
end) do
subject { @s }
end
it 'generates a NOT LIKE query with value surrounded by %' do
@s.name_not_cont = 'ric'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE '%ric%'/
end
end
describe 'i_cont' do
it_has_behavior 'wildcard escaping', :name_i_cont,
(if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
/"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
/LOWER\(`people`.`name`\) LIKE '%\\\\%.\\\\_\\\\\\\\%'/
else
/LOWER\("people"."name"\) LIKE '%%._\\%'/
end) do
subject { @s }
end
it 'generates a LIKE query with LOWER(column) and value surrounded by %' do
@s.name_i_cont = 'Ric'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /[LOWER\(]?#{field}\)? I?LIKE '%ric%'/
end
end
describe 'not_i_cont' do
it_has_behavior 'wildcard escaping', :name_not_i_cont,
(if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
/"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
/LOWER\(`people`.`name`\) NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
else
/LOWER\("people"."name"\) NOT LIKE '%%._\\%'/
end) do
subject { @s }
end
it 'generates a NOT LIKE query with LOWER(column) and value surrounded by %' do
@s.name_not_i_cont = 'Ric'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /[LOWER\(]?#{field}\)? NOT I?LIKE '%ric%'/
end
end
describe 'start' do
it 'generates a LIKE query with value followed by %' do
@s.name_start = 'Er'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE 'Er%'/
end
it "works with attribute names ending with '_start'" do
@s.new_start_start = 'hEy'
field = "#{quote_table_name("people")}.#{quote_column_name("new_start")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE 'hEy%'/
end
it "works with attribute names ending with '_end'" do
@s.stop_end_start = 'begin'
field = "#{quote_table_name("people")}.#{quote_column_name("stop_end")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE 'begin%'/
end
end
describe 'not_start' do
it 'generates a NOT LIKE query with value followed by %' do
@s.name_not_start = 'Eri'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE 'Eri%'/
end
it "works with attribute names ending with '_start'" do
@s.new_start_not_start = 'hEy'
field = "#{quote_table_name("people")}.#{quote_column_name("new_start")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE 'hEy%'/
end
it "works with attribute names ending with '_end'" do
@s.stop_end_not_start = 'begin'
field = "#{quote_table_name("people")}.#{quote_column_name("stop_end")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE 'begin%'/
end
end
describe 'end' do
it 'generates a LIKE query with value preceded by %' do
@s.name_end = 'Miller'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE '%Miller'/
end
it "works with attribute names ending with '_start'" do
@s.new_start_end = 'finish'
field = "#{quote_table_name("people")}.#{quote_column_name("new_start")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE '%finish'/
end
it "works with attribute names ending with '_end'" do
@s.stop_end_end = 'Ending'
field = "#{quote_table_name("people")}.#{quote_column_name("stop_end")}"
expect(@s.result.to_sql).to match /#{field} I?LIKE '%Ending'/
end
end
describe 'not_end' do
it 'generates a NOT LIKE query with value preceded by %' do
@s.name_not_end = 'Miller'
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE '%Miller'/
end
it "works with attribute names ending with '_start'" do
@s.new_start_not_end = 'finish'
field = "#{quote_table_name("people")}.#{quote_column_name("new_start")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE '%finish'/
end
it "works with attribute names ending with '_end'" do
@s.stop_end_not_end = 'Ending'
field = "#{quote_table_name("people")}.#{quote_column_name("stop_end")}"
expect(@s.result.to_sql).to match /#{field} NOT I?LIKE '%Ending'/
end
end
describe 'true' do
it 'generates an equality condition for boolean true' do
@s.awesome_true = true
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} = #{
ActiveRecord::Base.connection.quoted_true}/
end
it 'generates an inequality condition for boolean true' do
@s.awesome_true = false
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} != #{
ActiveRecord::Base.connection.quoted_true}/
end
end
describe 'not_true' do
it 'generates an inequality condition for boolean true' do
@s.awesome_not_true = true
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} != #{
ActiveRecord::Base.connection.quoted_true}/
end
it 'generates an equality condition for boolean true' do
@s.awesome_not_true = false
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} = #{
ActiveRecord::Base.connection.quoted_true}/
end
end
describe 'false' do
it 'generates an equality condition for boolean false' do
@s.awesome_false = true
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} = #{
ActiveRecord::Base.connection.quoted_false}/
end
it 'generates an inequality condition for boolean false' do
@s.awesome_false = false
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} != #{
ActiveRecord::Base.connection.quoted_false}/
end
end
describe 'not_false' do
it 'generates an inequality condition for boolean false' do
@s.awesome_not_false = true
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} != #{
ActiveRecord::Base.connection.quoted_false}/
end
it 'generates an equality condition for boolean false' do
@s.awesome_not_false = false
field = "#{quote_table_name("people")}.#{quote_column_name("awesome")}"
expect(@s.result.to_sql).to match /#{field} = #{
ActiveRecord::Base.connection.quoted_false}/
end
end
describe 'null' do
it 'generates a value IS NULL query' do
@s.name_null = true
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NULL/
end
it 'generates a value IS NOT NULL query when assigned false' do
@s.name_null = false
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NOT NULL/
end
end
describe 'not_null' do
it 'generates a value IS NOT NULL query' do
@s.name_not_null = true
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NOT NULL/
end
it 'generates a value IS NULL query when assigned false' do
@s.name_not_null = false
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NULL/
end
describe 'with association query' do
it 'generates a value IS NOT NULL query' do
@s.comments_id_not_null = true
sql = @s.result.to_sql
parent_field = "#{quote_table_name("people")}.#{quote_column_name("id")}"
expect(sql).to match /#{parent_field} IN/
field = "#{quote_table_name("comments")}.#{quote_column_name("id")}"
expect(sql).to match /#{field} IS NOT NULL/
expect(sql).not_to match /AND NOT/
end
it 'generates a value IS NULL query when assigned false' do
@s.comments_id_not_null = false
sql = @s.result.to_sql
parent_field = "#{quote_table_name("people")}.#{quote_column_name("id")}"
expect(sql).to match /#{parent_field} NOT IN/
field = "#{quote_table_name("comments")}.#{quote_column_name("id")}"
expect(sql).to match /#{field} IS NULL/
expect(sql).to match /AND NOT/
end
end
end
describe 'present' do
it %q[generates a value IS NOT NULL AND value != '' query] do
@s.name_present = true
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NOT NULL AND #{field} != ''/
end
it %q[generates a value IS NULL OR value = '' query when assigned false] do
@s.name_present = false
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NULL OR #{field} = ''/
end
end
describe 'blank' do
it %q[generates a value IS NULL OR value = '' query] do
@s.name_blank = true
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NULL OR #{field} = ''/
end
it %q[generates a value IS NOT NULL AND value != '' query when assigned false] do
@s.name_blank = false
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} IS NOT NULL AND #{field} != ''/
end
end
context "defining custom predicates" do
describe "with 'not_in' arel predicate" do
before do
Ransack.configure { |c| c.add_predicate "not_in_csv", arel_predicate: "not_in", formatter: proc { |v| v.split(",") } }
end
it 'generates a value IS NOT NULL query' do
@s.name_not_in_csv = ["a", "b"]
field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
expect(@s.result.to_sql).to match /#{field} NOT IN \('a', 'b'\)/
end
end
end
private
def test_boolean_equality_for(boolean_value)
query = expected_query(boolean_value)
test_values_for(boolean_value).each do |value|
s = Search.new(Person, awesome_eq: value)
expect(s.result.to_sql).to match query
end
end
def test_values_for(boolean_value)
case boolean_value
when true
TRUE_VALUES
when false
FALSE_VALUES
end
end
def expected_query(value, attribute = 'awesome', operator = '=')
field = "#{quote_table_name("people")}.#{quote_column_name(attribute)}"
quoted_value = ActiveRecord::Base.connection.quote(value)
/#{field} #{operator} #{quoted_value}/
end
end
end