Skip to content

Commit 78ec0e2

Browse files
committed
Add case sensitive parameter to string scopes
1 parent 10d93f5 commit 78ec0e2

File tree

4 files changed

+44
-4
lines changed

4 files changed

+44
-4
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ Transaction.amount_not_within(100, 200) # => where("amount <= 100 OR amount >= 2
3535

3636
# String scopes
3737
Transaction.description_contains("foo") # => where("description LIKE '%foo%'")
38+
Transaction.description_contains("foo", sensitive: false) # => where("description ILIKE '%foo%'")
3839
Transaction.description_starts_with("foo") # => where("description LIKE 'foo%'")
40+
Transaction.description_starts_with("foo", sensitive: false) # => where("description ILIKE 'foo%'")
3941
Transaction.description_ends_with("foo") # => where("description LIKE '%foo'")
42+
Transaction.description_ends_with("foo", sensitive: false) # => where("description ILIKE '%foo'")
43+
Transaction.description_like("%foo%") # => where("description LIKE '%foo%'")
44+
Transaction.description_ilike("%foo%") # => where("description ILIKE '%foo%'")
4045

4146
# Boolean scopes
4247
Transaction.non_profit # => where("non_profit = true")
@@ -49,7 +54,7 @@ Transaction.was_processed # => where("was_processed = true")
4954
Transaction.was_not_processed # => where("was_processed = false")
5055
```
5156

52-
For the string scope the pattern matching is escaped:
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.
5358

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

lib/type_scopes/string.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,19 @@ def self.escape(string)
1313

1414
def self.create_scopes_for_column(model, name)
1515
column = model.arel_table[name]
16-
append_scope(model, :"#{name}_contains", lambda { |str| where(column.matches("%" + TypeScopes::String.escape(str) + "%")) })
17-
append_scope(model, :"#{name}_starts_with", lambda { |str| where(column.matches(TypeScopes::String.escape(str) + "%")) })
18-
append_scope(model, :"#{name}_ends_with", lambda { |str| where(column.matches("%" + TypeScopes::String.escape(str))) })
16+
append_scope(model, :"#{name}_like", lambda { |str, sensitive: true| where(column.matches(str, nil, sensitive)) })
17+
append_scope(model, :"#{name}_ilike", lambda { |str| where(column.matches(str)) })
18+
19+
append_scope(model, :"#{name}_contains", lambda { |str, sensitive: true|
20+
send("#{name}_like", "%#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
21+
})
22+
23+
append_scope(model, :"#{name}_starts_with", lambda { |str, sensitive: true|
24+
send("#{name}_like", "#{TypeScopes::String.escape(str)}%", sensitive: sensitive)
25+
})
26+
27+
append_scope(model, :"#{name}_ends_with", lambda { |str, sensitive: true|
28+
send("#{name}_like", "%#{TypeScopes::String.escape(str)}", sensitive: sensitive)
29+
})
1930
end
2031
end

test/test_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def self.initialize_database
2929
TypeScopes::Transaction::Migration.new.up
3030
TypeScopes::Transaction.include(TypeScopes)
3131
end
32+
33+
def like_case_sensitive?
34+
# By default SQLite's like is case insensitive.
35+
# So it's not possible to have the exact same tests with other databases.
36+
ActiveRecord::Base.connection.adapter_name != "SQLite"
37+
end
3238
end
3339

3440
TypeScopes::TestCase.initialize_database

test/type_scopes/string_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,35 @@ def setup
77
TypeScopes::Transaction.create!(amount: 200, paid_at: "2021-06-24", description: "Last transaction")
88
end
99

10+
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)
15+
end
16+
17+
def test_ilike
18+
assert_equal(1, TypeScopes::Transaction.description_ilike("%First%").count)
19+
assert_equal(1, TypeScopes::Transaction.description_ilike("%FIRST%").count)
20+
end
21+
1022
def test_contains
1123
assert_equal(2, TypeScopes::Transaction.description_contains("t t").count)
1224
assert_equal(0, TypeScopes::Transaction.description_contains("xxx").count)
1325
end
1426

1527
def test_starts_with
1628
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)
1732
end
1833

1934
def test_ends_with
2035
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)
2139
end
2240

2341
def test_escaped_characters

0 commit comments

Comments
 (0)