Skip to content

Commit d69bc65

Browse files
committed
Add Regexp handler to predicate builder
This handler will expose basic regular expression conditions to the query interface. `Regexp`s with modifiers are not supported. `matches_regexp` was implemented in SQLite3 to support this for all adapters. Book.where(title: /The/)
1 parent ce35980 commit d69bc65

File tree

6 files changed

+102
-0
lines changed

6 files changed

+102
-0
lines changed

activerecord/CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
2+
* Support Regexp values in query hash conditions.
3+
4+
Active Record now supports accepting a Regexp without modifiers in a
5+
hash condition. This will generate SQL using per-database adapter
6+
regular expression operators.
7+
8+
```ruby
9+
Book.where(title: /The/)
10+
```
11+
12+
*Jenny Shen*
13+
114
* Allow bypassing primary key/constraint addition in `implicit_order_column`
215

316
When specifying multiple columns in an array for `implicit_order_column`, adding

activerecord/lib/active_record/relation/predicate_builder.rb

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class PredicateBuilder # :nodoc:
55
require "active_record/relation/predicate_builder/array_handler"
66
require "active_record/relation/predicate_builder/basic_object_handler"
77
require "active_record/relation/predicate_builder/range_handler"
8+
require "active_record/relation/predicate_builder/regexp_handler"
89
require "active_record/relation/predicate_builder/relation_handler"
910
require "active_record/relation/predicate_builder/association_query_value"
1011
require "active_record/relation/predicate_builder/polymorphic_array_value"
@@ -18,6 +19,7 @@ def initialize(table)
1819
register_handler(Relation, RelationHandler.new)
1920
register_handler(Array, ArrayHandler.new(self))
2021
register_handler(Set, ArrayHandler.new(self))
22+
register_handler(Regexp, RegexpHandler.new(self))
2123
end
2224

2325
def build_from_hash(attributes, &block)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
class PredicateBuilder
5+
class RegexpHandler # :nodoc:
6+
def initialize(predicate_builder)
7+
@predicate_builder = predicate_builder
8+
end
9+
10+
def call(attribute, value)
11+
unless value.options == 0
12+
raise ArgumentError, "Regexp for #{attribute.name} must not have modifiers"
13+
end
14+
15+
attribute.matches_regexp(value.source)
16+
end
17+
18+
private
19+
attr_reader :predicate_builder
20+
end
21+
end
22+
end

activerecord/lib/arel/visitors/sqlite.rb

+10
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ def prepare_update_statement(o)
7171
end
7272
end
7373

74+
# REGEXP operator uses regexp() user function. This function is not defined by default.
75+
# See: https://www.sqlite.org/lang_expr.html#the_like_glob_regexp_match_and_extract_operators
76+
def visit_Arel_Nodes_Regexp(o, collector)
77+
infix_value o, collector, " REGEXP "
78+
end
79+
80+
def visit_Arel_Nodes_NotRegexp(o, collector)
81+
infix_value o, collector, " NOT REGEXP "
82+
end
83+
7484
# Locks are not supported in SQLite
7585
def visit_Arel_Nodes_Lock(o, collector)
7686
collector

activerecord/test/cases/arel/visitors/sqlite_test.rb

+38
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,44 @@ def compile(node)
7070
_(sql).must_be_like %{ "users"."name" IS NOT NULL }
7171
end
7272
end
73+
74+
describe "Nodes::Regexp" do
75+
it "should know how to visit" do
76+
node = Table.new(:users)[:name].matches_regexp("foo.*")
77+
_(node).must_be_kind_of Nodes::Regexp
78+
_(compile(node)).must_be_like %{
79+
"users"."name" REGEXP 'foo.*'
80+
}
81+
end
82+
83+
it "can handle subqueries" do
84+
table = Table.new(:users)
85+
subquery = table.project(:id).where(table[:name].matches_regexp("foo.*"))
86+
node = table[:id].in subquery
87+
_(compile(node)).must_be_like %{
88+
"users"."id" IN (SELECT id FROM "users" WHERE "users"."name" REGEXP 'foo.*')
89+
}
90+
end
91+
end
92+
93+
describe "Nodes::NotRegexp" do
94+
it "should know how to visit" do
95+
node = Table.new(:users)[:name].does_not_match_regexp("foo.*")
96+
_(node).must_be_kind_of Nodes::NotRegexp
97+
_(compile(node)).must_be_like %{
98+
"users"."name" NOT REGEXP 'foo.*'
99+
}
100+
end
101+
102+
it "can handle subqueries" do
103+
table = Table.new(:users)
104+
subquery = table.project(:id).where(table[:name].does_not_match_regexp("foo.*"))
105+
node = table[:id].in subquery
106+
_(compile(node)).must_be_like %{
107+
"users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT REGEXP 'foo.*')
108+
}
109+
end
110+
end
73111
end
74112
end
75113
end

activerecord/test/cases/relation/where_test.rb

+17
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,23 @@ def test_rewhere_on_root
9292
assert_equal posts(:welcome), Post.rewhere(title: "Welcome to the weblog").first
9393
end
9494

95+
# SQLite does not support regexp() user function by default.
96+
unless current_adapter?(:SQLite3Adapter)
97+
def test_where_with_regexp
98+
expected_comments = [comments(:greetings), comments(:more_greetings)]
99+
100+
assert_equal expected_comments, Comment.where(body: /Thank you( again)? for the welcome/).to_a
101+
end
102+
103+
def test_where_with_regexp_with_options
104+
error = assert_raise ArgumentError do
105+
Comment.where(body: /Thank you( again)? for the welcome/i)
106+
end
107+
108+
assert_equal "Regexp for body must not have modifiers", error.message
109+
end
110+
end
111+
95112
def test_where_with_tuple_syntax
96113
first_topic = topics(:first)
97114
third_topic = topics(:third)

0 commit comments

Comments
 (0)