diff --git a/app/models/query.rb b/app/models/query.rb index 289d683f6..9eccd4a75 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -313,6 +313,7 @@ class Query < ActiveRecord::Base "!p" => :label_no_issues_in_project, "*o" => :label_any_open_issues, "!o" => :label_no_open_issues, + "/" => :label_matches_regexp } class_attribute :operators_by_filter_type @@ -323,14 +324,18 @@ class Query < ActiveRecord::Base :list_subprojects => [ "*", "!*", "=", "!" ], :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "~", "=", "!~", "!", "^", "$", "!*", "*" ], - :text => [ "~", "!~", "^", "$", "!*", "*" ], + :string => [ "~", "=", "/", "!~", "!", "^", "$", "!*", "*" ], + :text => [ "~", "/", "!~", "^", "$", "!*", "*" ], :search => [ "~", "!~" ], :integer => [ "=", ">=", "<=", "><", "!*", "*" ], :float => [ "=", ">=", "<=", "><", "!*", "*" ], :relation => ["=", "!", "=p", "=!p", "!p", "*o", "!o", "!*", "*"], :tree => ["=", "~", "!*", "*"] } + unless Redmine::Database.supports_regexp? + operators_by_filter_type[:string].delete('/') + operators_by_filter_type[:text].delete('/') + end class_attribute :available_columns self.available_columns = [] @@ -1435,6 +1440,11 @@ class Query < ActiveRecord::Base sql = sql_contains("#{db_table}.#{db_field}", value.first, :starts_with => true) when "$" sql = sql_contains("#{db_table}.#{db_field}", value.first, :ends_with => true) + when "/" + operator = Redmine::Database.regexp_operator + unless operator.nil? + sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} #{operator} ?", value]) + end else raise QueryError, "Unknown query operator #{operator}" end diff --git a/config/locales/en.yml b/config/locales/en.yml index 8c9edb8bc..3da2af3e6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -813,6 +813,7 @@ en: label_not_contains: doesn't contain label_starts_with: starts with label_ends_with: ends with + label_matches_regexp: matches regexp label_any_issues_in_project: any issues in project label_any_issues_not_in_project: any issues not in project label_no_issues_in_project: no issues in project diff --git a/lib/redmine/database.rb b/lib/redmine/database.rb index a3c12a4a6..50d90157d 100644 --- a/lib/redmine/database.rb +++ b/lib/redmine/database.rb @@ -61,6 +61,11 @@ module Redmine /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) end + # Returns the MysQL version or nil if another DBMS is used + def mysql_version + mysql? ? ActiveRecord::Base.connection.select_value('SELECT VERSION()').to_s : nil + end + # Returns a SQL statement for case/accent (if possible) insensitive match def like(left, right, options={}) neg = (options[:match] == false ? 'NOT ' : '') @@ -103,6 +108,24 @@ module Redmine def reset @postgresql_unaccent = nil end + + # Returns true if the database supports regular expressions + def supports_regexp? + regexp_operator.present? + end + + # Returns the regexp operator for the current database + # MySQL 8.0.4+ and PostgreSQL are supported + def regexp_operator + @regexp_operator ||= + if mysql? && Gem::Version.new(mysql_version) >= Gem::Version.new('8.0.4') + 'REGEXP' + elsif postgresql? + '~*' + else + nil + end + end end end end diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 607679e83..95ed819c0 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -710,6 +710,17 @@ class QueryTest < ActiveSupport::TestCase assert_not_include issue, result end + def test_operator_regexp + skip unless Redmine::Database.supports_regexp? + + regexp_str = '(Recipes{0,1}|ingred.ents)' + query = IssueQuery.new(:name => '_') + query.add_filter('subject', '/', [regexp_str]) + result = find_issues_with_query(query) + assert_equal [1, 2, 3], result.map(&:id).sort + result.each {|issue| assert issue.subject =~ /#{regexp_str}/i} + end + def test_range_for_this_week_with_week_starting_on_monday I18n.locale = :fr assert_equal '1', I18n.t(:general_first_day_of_week)