Skip to content

Commit e08cf2a

Browse files
committed
Add more string scopes
column_does_not_contain column_does_not_starts_with column_does_not_ends_with column_matches column_does_not_matches
1 parent 78ec0e2 commit e08cf2a

File tree

4 files changed

+97
-19
lines changed

4 files changed

+97
-19
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,18 @@ Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
3838
Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
3939
Transaction.description_starts_with("foo") # => where("description LIKE 'foo%'")
4040
Transaction.description_starts_with("foo", sensitive: false) # => where("description ILIKE 'foo%'")
41+
Transaction.description_does_not_start_with("foo") # => where("description NOT LIKE 'foo%'")
42+
Transaction.description_does_not_start_with("foo", sensitive: false) # => where("description NOT ILIKE 'foo%'")
4143
Transaction.description_ends_with("foo") # => where("description LIKE '%foo'")
4244
Transaction.description_ends_with("foo", sensitive: false) # => where("description ILIKE '%foo'")
45+
Transaction.description_does_not_end_with("foo") # => where("description NOT LIKE '%foo'")
46+
Transaction.description_does_not_end_with("foo", sensitive: false) # => where("description NOT ILIKE '%foo'")
4347
Transaction.description_like("%foo%") # => where("description LIKE '%foo%'")
48+
Transaction.description_not_like("%foo%") # => where("description NOT LIKE '%foo%'")
4449
Transaction.description_ilike("%foo%") # => where("description ILIKE '%foo%'")
50+
Transaction.description_not_ilike("%foo%") # => where("description NOT ILIKE '%foo%'")
51+
Transaction.description_matches("^Regex$") # => where("description ~ '^Regex$'")
52+
Transaction.description_does_not_match("^Regex$") # => where("description !~ '^Regex$'")
4553

4654
# Boolean scopes
4755
Transaction.non_profit # => where("non_profit = true")
@@ -54,7 +62,7 @@ Transaction.was_processed # => where("was_processed = true")
5462
Transaction.was_not_processed # => where("was_processed = false")
5563
```
5664

57-
For the string colums, the pattern matching is escaped. So it's safe to provide directly a user input. There is an exception for the `column_like` and `column_ilike` where the pattern is not escaped and you shouldn't provide untrusted strings.
65+
For the string colums, the pattern matching is escaped. So it's safe to provide directly a user input. There is an exception for the `column_like`, `column_ilike`. `column_matches` and `column_does_not_match` where the pattern is not escaped and you shouldn't provide untrusted strings.
5866

5967
```ruby
6068
Transaction.description_contains("%foo_") # => where("description LIKE '%[%]foo[_]%'")

lib/type_scopes/string.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,39 @@ def self.escape(string)
1414
def self.create_scopes_for_column(model, name)
1515
column = model.arel_table[name]
1616
append_scope(model, :"#{name}_like", lambda { |str, sensitive: true| where(column.matches(str, nil, sensitive)) })
17+
append_scope(model, :"#{name}_not_like", lambda { |str, sensitive: true| where(column.does_not_match(str, nil, sensitive)) })
1718
append_scope(model, :"#{name}_ilike", lambda { |str| where(column.matches(str)) })
19+
append_scope(model, :"#{name}_not_ilike", lambda { |str| where(column.does_not_match(str)) })
1820

1921
append_scope(model, :"#{name}_contains", lambda { |str, sensitive: true|
2022
send("#{name}_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
2123
})
2224

25+
append_scope(model, :"#{name}_does_not_contain", lambda { |str, sensitive: true|
26+
send("#{name}_not_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
27+
})
28+
29+
append_scope(model, :"#{name}_does_not_contain", lambda { |str, sensitive: true|
30+
send("#{name}_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
31+
})
32+
2333
append_scope(model, :"#{name}_starts_with", lambda { |str, sensitive: true|
2434
send("#{name}_like", "#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
2535
})
2636

37+
append_scope(model, :"#{name}_does_not_start_with", lambda { |str, sensitive: true|
38+
send("#{name}_not_like", "#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
39+
})
40+
2741
append_scope(model, :"#{name}_ends_with", lambda { |str, sensitive: true|
2842
send("#{name}_like", "%#{TypeScopes::String.escape(str)}", sensitive: sensitive)
2943
})
44+
45+
append_scope(model, :"#{name}_does_not_end_with", lambda { |str, sensitive: true|
46+
send("#{name}_not_like", "%#{TypeScopes::String.escape(str)}", sensitive: sensitive)
47+
})
48+
49+
append_scope(model, :"#{name}_matches", lambda { |str, sensitive: true| where(column.matches_regexp(str, sensitive)) })
50+
append_scope(model, :"#{name}_does_not_match", lambda { |str, sensitive: true| where(column.does_not_match_regexp(str, sensitive)) })
3051
end
3152
end

test/test_helper.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ def self.initialize_database
3030
TypeScopes::Transaction.include(TypeScopes)
3131
end
3232

33-
def like_case_sensitive?
33+
def sql_adapter_like_case_sensitive?
3434
# By default SQLite's like is case insensitive.
3535
# So it's not possible to have the exact same tests with other databases.
3636
ActiveRecord::Base.connection.adapter_name != "SQLite"
3737
end
38+
39+
def sql_adapter_supports_regex?
40+
ActiveRecord::Base.connection.adapter_name != "SQLite"
41+
end
3842
end
3943

4044
TypeScopes::TestCase.initialize_database

test/type_scopes/string_test.rb

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,88 @@
33
class TypeScopes::StringTest < TypeScopes::TestCase
44
def setup
55
TypeScopes::Transaction.connection.truncate(TypeScopes::Transaction.table_name)
6-
TypeScopes::Transaction.create!(amount: 100, paid_at: "2021-06-23", description: "First transaction")
7-
TypeScopes::Transaction.create!(amount: 200, paid_at: "2021-06-24", description: "Last transaction")
6+
TypeScopes::Transaction.create!(amount: 100, paid_at: "2021-06-23", description: "Lorem ipsum")
7+
TypeScopes::Transaction.create!(amount: 200, paid_at: "2021-06-24", description: "Lorem ipsum")
88
end
99

1010
def test_like
11-
assert_equal(1, TypeScopes::Transaction.description_like("%First%").count)
12-
return unless like_case_sensitive?
13-
assert_equal(0, TypeScopes::Transaction.description_like("%FIRST%").count)
14-
assert_equal(1, TypeScopes::Transaction.description_like("%FIRST%", sensitive: false).count)
11+
assert_equal(2, TypeScopes::Transaction.description_like("%Lorem%").count)
12+
return unless sql_adapter_like_case_sensitive?
13+
assert_equal(0, TypeScopes::Transaction.description_like("%LOREM%").count)
14+
assert_equal(2, TypeScopes::Transaction.description_like("%LOREM%", sensitive: false).count)
15+
end
16+
17+
def test_not_like
18+
assert_equal(0, TypeScopes::Transaction.description_not_like("%ipsum").count)
19+
return unless sql_adapter_like_case_sensitive?
20+
assert_equal(2, TypeScopes::Transaction.description_not_like("%IPSUM").count)
21+
assert_equal(0, TypeScopes::Transaction.description_not_like("%IPSUM", sensitive: false).count)
1522
end
1623

1724
def test_ilike
18-
assert_equal(1, TypeScopes::Transaction.description_ilike("%First%").count)
19-
assert_equal(1, TypeScopes::Transaction.description_ilike("%FIRST%").count)
25+
assert_equal(0, TypeScopes::Transaction.description_ilike("%xxx%").count)
26+
assert_equal(2, TypeScopes::Transaction.description_ilike("LOREM%").count)
27+
end
28+
29+
def test_not_ilike
30+
assert_equal(0, TypeScopes::Transaction.description_not_ilike("%IPSUM").count)
31+
assert_equal(2, TypeScopes::Transaction.description_not_ilike("%xxx%").count)
2032
end
2133

2234
def test_contains
23-
assert_equal(2, TypeScopes::Transaction.description_contains("t t").count)
35+
assert_equal(2, TypeScopes::Transaction.description_contains("m i").count)
2436
assert_equal(0, TypeScopes::Transaction.description_contains("xxx").count)
2537
end
2638

39+
def test_does_not_contain
40+
assert_equal(0, TypeScopes::Transaction.description_does_not_contain("m i").count)
41+
assert_equal(2, TypeScopes::Transaction.description_does_not_contain("xxx").count)
42+
end
43+
2744
def test_starts_with
28-
assert_equal(1, TypeScopes::Transaction.description_starts_with("First").count)
29-
assert_equal(1, TypeScopes::Transaction.description_starts_with("FIRST", sensitive: false).count)
30-
return unless like_case_sensitive?
31-
assert_equal(0, TypeScopes::Transaction.description_starts_with("FIRST").count)
45+
assert_equal(2, TypeScopes::Transaction.description_starts_with("Lorem").count)
46+
assert_equal(2, TypeScopes::Transaction.description_starts_with("LOREM", sensitive: false).count)
47+
return unless sql_adapter_like_case_sensitive?
48+
assert_equal(0, TypeScopes::Transaction.description_starts_with("LOREM").count)
49+
end
50+
51+
def test_does_not_start_with
52+
assert_equal(0, TypeScopes::Transaction.description_does_not_start_with("Lorem").count)
53+
assert_equal(0, TypeScopes::Transaction.description_does_not_start_with("LOREM", sensitive: false).count)
54+
return unless sql_adapter_like_case_sensitive?
55+
assert_equal(2, TypeScopes::Transaction.description_does_not_start_with("LOREM").count)
3256
end
3357

3458
def test_ends_with
35-
assert_equal(2, TypeScopes::Transaction.description_ends_with("tion").count)
36-
assert_equal(2, TypeScopes::Transaction.description_ends_with("TION", sensitive: false).count)
37-
return unless like_case_sensitive?
38-
assert_equal(0, TypeScopes::Transaction.description_ends_with("TION").count)
59+
assert_equal(2, TypeScopes::Transaction.description_ends_with("ipsum").count)
60+
assert_equal(2, TypeScopes::Transaction.description_ends_with("IPSUM", sensitive: false).count)
61+
return unless sql_adapter_like_case_sensitive?
62+
assert_equal(0, TypeScopes::Transaction.description_ends_with("IPSUM").count)
63+
end
64+
65+
def test_does_not_end_with
66+
assert_equal(0, TypeScopes::Transaction.description_does_not_end_with("ipsum").count)
67+
assert_equal(0, TypeScopes::Transaction.description_does_not_end_with("IPSUM", sensitive: false).count)
68+
return unless sql_adapter_like_case_sensitive?
69+
assert_equal(2, TypeScopes::Transaction.description_does_not_end_with("IPSUM").count)
3970
end
4071

4172
def test_escaped_characters
4273
assert_equal(0, TypeScopes::Transaction.description_contains("%").count)
4374
assert_equal(0, TypeScopes::Transaction.description_contains("_").count)
4475
end
76+
77+
def test_matches
78+
skip unless sql_adapter_supports_regex?
79+
assert_equal(2, TypeScopes::Transaction.description_matches("Lorem.").count)
80+
assert_equal(2, TypeScopes::Transaction.description_matches("LOREM.", sensitive: false).count)
81+
assert_equal(0, TypeScopes::Transaction.description_matches("LOREM.").count)
82+
end
83+
84+
def test_does_not_match
85+
skip unless sql_adapter_supports_regex?
86+
assert_equal(0, TypeScopes::Transaction.description_does_not_match("Lorem.").count)
87+
assert_equal(0, TypeScopes::Transaction.description_does_not_match("LOREM.", sensitive: false).count)
88+
assert_equal(2, TypeScopes::Transaction.description_does_not_match("LOREM.").count)
89+
end
4590
end

0 commit comments

Comments
 (0)