diff --git a/lib/ransack/constants.rb b/lib/ransack/constants.rb index 027e87be3..f8bfb2000 100644 --- a/lib/ransack/constants.rb +++ b/lib/ransack/constants.rb @@ -161,16 +161,8 @@ module Constants module_function # replace % \ to \% \\ def escape_wildcards(unescaped) - case ActiveRecord::Base.connection.adapter_name - when "Mysql2".freeze - # Necessary for MySQL - unescaped.to_s.gsub(/([\\%_])/, '\\\\\\1') - when "PostGIS".freeze, "PostgreSQL".freeze - # Necessary for PostgreSQL - unescaped.to_s.gsub(/([\\%_.])/, '\\\\\\1') - else - unescaped - end + # Use ActiveRecord's sanitize_sql_like for consistent escaping across all adapters + ActiveRecord::Base.sanitize_sql_like(unescaped.to_s) end end end diff --git a/lib/ransack/nodes/condition.rb b/lib/ransack/nodes/condition.rb index 0dc01eb01..f3163191b 100644 --- a/lib/ransack/nodes/condition.rb +++ b/lib/ransack/nodes/condition.rb @@ -324,7 +324,13 @@ def combinator_method def format_predicate(attribute) arel_pred = arel_predicate_for_attribute(attribute) arel_values = formatted_values_for_attribute(attribute) - predicate = attr_value_for_attribute(attribute).public_send(arel_pred, arel_values) + + # For LIKE predicates that use escaped wildcards, add ESCAPE clause for proper behavior + if needs_escape_clause?(arel_pred, arel_values) + predicate = attr_value_for_attribute(attribute).public_send(arel_pred, arel_values, '\\') + else + predicate = attr_value_for_attribute(attribute).public_send(arel_pred, arel_values) + end if in_predicate?(predicate) predicate.right = predicate.right.map do |pr| @@ -364,6 +370,15 @@ def replace_right_node?(predicate) def valid_combinator? attributes.size < 2 || Constants::AND_OR.include?(combinator) end + + def needs_escape_clause?(arel_pred, arel_values) + # Add ESCAPE clause for LIKE predicates when values contain escaped wildcards + like_predicates = ['matches', 'does_not_match'] + return false unless like_predicates.include?(arel_pred.to_s) + + # Check if any values contain escaped wildcards (backslash followed by %, _, or \) + Array(arel_values).any? { |val| val.to_s.match?(/\\[%_\\]/) } + end end end end diff --git a/spec/ransack/predicate_spec.rb b/spec/ransack/predicate_spec.rb index cf84e200d..c9a1b777b 100644 --- a/spec/ransack/predicate_spec.rb +++ b/spec/ransack/predicate_spec.rb @@ -21,6 +21,45 @@ module Ransack end end + describe 'wildcard escaping behavior' do + context 'with special characters in search values' do + it 'properly escapes wildcard characters in LIKE predicates' do + # Test that % and _ are treated as literal characters + Person.create!(name: '100% Pure') + Person.create!(name: '50_50 Blend') + Person.create!(name: 'Normal Text') + + # Search for literal % + results = Person.ransack(name_cont: '%').result + expect(results).to include(Person.find_by(name: '100% Pure')) + expect(results).not_to include(Person.find_by(name: 'Normal Text')) + + # Search for literal _ + results = Person.ransack(name_cont: '_').result + expect(results).to include(Person.find_by(name: '50_50 Blend')) + expect(results).not_to include(Person.find_by(name: 'Normal Text')) + end + + it 'works with start predicate' do + Person.create!(name: '%start') + Person.create!(name: 'normalstart') + + results = Person.ransack(name_start: '%').result + expect(results).to include(Person.find_by(name: '%start')) + expect(results).not_to include(Person.find_by(name: 'normalstart')) + end + + it 'works with end predicate' do + Person.create!(name: 'end%') + Person.create!(name: 'endnormal') + + results = Person.ransack(name_end: '%').result + expect(results).to include(Person.find_by(name: 'end%')) + expect(results).not_to include(Person.find_by(name: 'endnormal')) + end + end + end + describe 'eq' do it 'generates an equality condition for boolean true values' do test_boolean_equality_for(true) @@ -163,7 +202,7 @@ module Ransack when "Mysql2" /`people`.`name` LIKE '%\\\\%.\\\\_\\\\\\\\%'/ else - /"people"."name" LIKE '%%._\\%'/ + /LIKE '%\\%\.\\_\\\\%' ESCAPE '\\'/ end) do subject { @s } end @@ -183,7 +222,7 @@ module Ransack when "Mysql2" /`people`.`name` NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/ else - /"people"."name" NOT LIKE '%%._\\%'/ + /NOT LIKE '%\\%\.\\_\\\\%' ESCAPE '\\'/ end) do subject { @s } end @@ -205,7 +244,7 @@ module Ransack when "Mysql2" /LOWER\(`people`.`name`\) LIKE '%\\\\%.\\\\_\\\\\\\\%'/ else - /LOWER\("people"."name"\) LIKE '%%._\\%'/ + /LIKE '%\\%\.\\_\\\\%' ESCAPE '\\'/ end) do subject { @s } end @@ -227,7 +266,7 @@ module Ransack when "Mysql2" /LOWER\(`people`.`name`\) NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/ else - /LOWER\("people"."name"\) NOT LIKE '%%._\\%'/ + /NOT LIKE '%\\%\.\\_\\\\%' ESCAPE '\\'/ end) do subject { @s } end