search_cop 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +42 -0
  3. data/.rubocop.yml +128 -0
  4. data/CHANGELOG.md +36 -5
  5. data/CONTRIBUTING.md +18 -0
  6. data/Gemfile +4 -17
  7. data/README.md +143 -35
  8. data/Rakefile +0 -1
  9. data/docker-compose.yml +18 -0
  10. data/gemfiles/rails5.gemfile +13 -0
  11. data/gemfiles/rails6.gemfile +13 -0
  12. data/lib/search_cop.rb +15 -13
  13. data/lib/search_cop/grammar_parser.rb +3 -4
  14. data/lib/search_cop/hash_parser.rb +23 -17
  15. data/lib/search_cop/helpers.rb +15 -0
  16. data/lib/search_cop/query_builder.rb +2 -4
  17. data/lib/search_cop/query_info.rb +0 -2
  18. data/lib/search_cop/search_scope.rb +8 -5
  19. data/lib/search_cop/version.rb +1 -1
  20. data/lib/search_cop/visitors.rb +0 -2
  21. data/lib/search_cop/visitors/mysql.rb +9 -7
  22. data/lib/search_cop/visitors/postgres.rb +18 -8
  23. data/lib/search_cop/visitors/visitor.rb +13 -6
  24. data/lib/search_cop_grammar.rb +18 -9
  25. data/lib/search_cop_grammar.treetop +6 -4
  26. data/lib/search_cop_grammar/attributes.rb +77 -34
  27. data/lib/search_cop_grammar/nodes.rb +7 -2
  28. data/search_cop.gemspec +9 -10
  29. data/test/and_test.rb +6 -8
  30. data/test/boolean_test.rb +7 -9
  31. data/test/database.yml +4 -1
  32. data/test/date_test.rb +38 -12
  33. data/test/datetime_test.rb +45 -12
  34. data/test/default_operator_test.rb +51 -0
  35. data/test/error_test.rb +2 -4
  36. data/test/float_test.rb +16 -11
  37. data/test/fulltext_test.rb +8 -10
  38. data/test/hash_test.rb +39 -31
  39. data/test/integer_test.rb +9 -11
  40. data/test/not_test.rb +6 -8
  41. data/test/or_test.rb +8 -10
  42. data/test/scope_test.rb +11 -13
  43. data/test/search_cop_test.rb +36 -34
  44. data/test/string_test.rb +67 -19
  45. data/test/test_helper.rb +24 -18
  46. data/test/visitor_test.rb +15 -8
  47. metadata +41 -55
  48. data/.travis.yml +0 -34
  49. data/Appraisals +0 -20
  50. data/gemfiles/3.2.gemfile +0 -26
  51. data/gemfiles/4.0.gemfile +0 -26
  52. data/gemfiles/4.1.gemfile +0 -26
  53. data/gemfiles/4.2.gemfile +0 -26
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8cb7e91b305057b2ea3df0c96643f8cd2cc726583f2362db9d576ef128826a75
4
+ data.tar.gz: 419ee38ec698795d478f7749486e63b533c9cc497057aa820b5627b343cbdeb4
5
+ SHA512:
6
+ metadata.gz: 19b765a13b9a21a5b798b60618253729b4a8c1279babe47644d5c030b93b09c87706138ab11cd5bfd9daee757d8d04bff95fc1bc3ce50c5f6f7399aaaabc09d1
7
+ data.tar.gz: a65e72e3d6e5500b986bb312c45dbbd00432419b344765c71387432dd127efd5eb463cd42f71c3e405913a66b8d45201b9d909b9e66da17bf28f11ea3b7aa5c8
@@ -0,0 +1,42 @@
1
+ name: test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ ruby: ["2.5", "2.6", "2.7"]
9
+ rails: ["rails5", "rails6"]
10
+ database: ["sqlite", "postgres", "mysql"]
11
+ services:
12
+ postgres:
13
+ image: postgres
14
+ env:
15
+ POSTGRES_USER: search_cop
16
+ POSTGRES_PASSWORD: secret
17
+ POSTGRES_DB: search_cop
18
+ ports:
19
+ - 5432:5432
20
+ mysql:
21
+ image: mysql
22
+ env:
23
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
24
+ MYSQL_ROOT_PASSWORD: ""
25
+ MYSQL_DATABASE: search_cop
26
+ ports:
27
+ - 3306:3306
28
+ steps:
29
+ - uses: actions/checkout@v1
30
+ - uses: actions/setup-ruby@v1
31
+ with:
32
+ ruby-version: ${{ matrix.ruby }}
33
+ - name: test
34
+ env:
35
+ DATABASE: ${{ matrix.database }}
36
+ run: |
37
+ gem install bundler
38
+ bundle config set --local gemfile "gemfiles/${{ matrix.rails }}.gemfile"
39
+ bundle config set --local path "../vendor/bundle"
40
+ bundle install
41
+ bundle exec rake test
42
+ bundle exec rubocop
data/.rubocop.yml ADDED
@@ -0,0 +1,128 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Gemspec/RequiredRubyVersion:
5
+ Enabled: false
6
+
7
+ Style/CaseLikeIf:
8
+ Enabled: false
9
+
10
+ Style/RedundantArgument:
11
+ Enabled: false
12
+
13
+ Lint/EmptyBlock:
14
+ Enabled: false
15
+
16
+ Layout/EmptyLineBetweenDefs:
17
+ Enabled: false
18
+
19
+ Style/FrozenStringLiteralComment:
20
+ Enabled: false
21
+
22
+ Lint/RedundantRequireStatement:
23
+ Enabled: false
24
+
25
+ Layout/ArgumentAlignment:
26
+ EnforcedStyle: with_fixed_indentation
27
+
28
+ Layout/FirstArrayElementIndentation:
29
+ EnforcedStyle: consistent
30
+
31
+ Style/PercentLiteralDelimiters:
32
+ Enabled: false
33
+
34
+ Style/SpecialGlobalVars:
35
+ EnforcedStyle: use_english_names
36
+
37
+ Security/Eval:
38
+ Enabled: false
39
+
40
+ Style/WordArray:
41
+ EnforcedStyle: brackets
42
+
43
+ Style/ClassAndModuleChildren:
44
+ Enabled: false
45
+
46
+ Style/TrivialAccessors:
47
+ Enabled: false
48
+
49
+ Style/Alias:
50
+ Enabled: false
51
+
52
+ Style/StringLiteralsInInterpolation:
53
+ EnforcedStyle: double_quotes
54
+
55
+ Metrics/ClassLength:
56
+ Enabled: false
57
+
58
+ Naming/MethodParameterName:
59
+ Enabled: false
60
+
61
+ Style/SymbolArray:
62
+ EnforcedStyle: brackets
63
+
64
+ Layout/RescueEnsureAlignment:
65
+ Enabled: false
66
+
67
+ Layout/LineLength:
68
+ Enabled: false
69
+
70
+ Metrics/MethodLength:
71
+ Enabled: false
72
+
73
+ Metrics/ModuleLength:
74
+ Enabled: false
75
+
76
+ Style/ZeroLengthPredicate:
77
+ Enabled: false
78
+
79
+ Metrics/PerceivedComplexity:
80
+ Enabled: false
81
+
82
+ Metrics/AbcSize:
83
+ Enabled: false
84
+
85
+ Metrics/CyclomaticComplexity:
86
+ Enabled: false
87
+
88
+ Metrics/BlockLength:
89
+ Enabled: false
90
+
91
+ Metrics/BlockNesting:
92
+ Enabled: false
93
+
94
+ Style/NumericPredicate:
95
+ Enabled: false
96
+
97
+ Naming/AccessorMethodName:
98
+ Enabled: false
99
+
100
+ Naming/MemoizedInstanceVariableName:
101
+ Enabled: false
102
+
103
+ Style/StringLiterals:
104
+ EnforcedStyle: double_quotes
105
+
106
+ Style/Documentation:
107
+ Enabled: false
108
+
109
+ Naming/ConstantName:
110
+ Enabled: false
111
+
112
+ Style/MutableConstant:
113
+ Enabled: false
114
+
115
+ Layout/MultilineMethodCallIndentation:
116
+ EnforcedStyle: indented
117
+
118
+ Layout/ParameterAlignment:
119
+ EnforcedStyle: with_fixed_indentation
120
+
121
+ Lint/UnusedMethodArgument:
122
+ Enabled: false
123
+
124
+ Style/IfUnlessModifier:
125
+ Enabled: false
126
+
127
+ Style/RedundantBegin:
128
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,6 +1,37 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ Version 1.2.0:
5
+
6
+ * Added support for disabling the right wildcard
7
+ * Fixed escaping of wildcard chars (`_`, `%`)
8
+
9
+ Version 1.1.0:
10
+
11
+ * Adds customizable default operator to concatenate conditions (#49)
12
+ * Make the postgis adapter use the postgres extensions
13
+
14
+ Version 1.0.9:
15
+
16
+ * Use `[:blank:]` instead of `\s` for space (#46)
17
+ * Updated `SearchCop::Visitors::Visitor` to check the connection's `adapter_name` when extending. (#47)
18
+ * Fix for negative numeric values
19
+ * allow searching for relative dates, like hours, days, weeks, months or years ago
20
+
21
+ Version 1.0.8:
22
+
23
+ * No longer add search scope methods globally #34
24
+
25
+ Version 1.0.7:
26
+
27
+ * Bugfix regarding `NOT` queries in fulltext mode #32
28
+ * Safely handle `NULL` values for match queries
29
+ * Added coalesce option
30
+
31
+ Version 1.0.6:
32
+
33
+ * Fixes a bug regarding date overflows in PostgreSQL
34
+
4
35
  Version 1.0.5:
5
36
 
6
37
  * Fixes a bug regarding quotation
@@ -32,7 +63,7 @@ Version 1.0.0:
32
63
 
33
64
  Version 0.0.5:
34
65
 
35
- * Supporting :default => false
66
+ * Supporting `:default => false`
36
67
  * Datetime/Date greater operator fix
37
68
  * Use reflection to find associated models
38
69
  * Providing reflection
@@ -41,16 +72,16 @@ Version 0.0.4:
41
72
 
42
73
  * Fixed date attributes
43
74
  * Fail softly for mixed datatype attributes
44
- * Support custom table, class and alias names via attr_searchable_alias
75
+ * Support custom table, class and alias names via `attr_searchable_alias`
45
76
 
46
77
  Version 0.0.3:
47
78
 
48
- * belongs_to association fixes
79
+ * `belongs_to` association fixes
49
80
 
50
81
  Version 0.0.2:
51
82
 
52
83
  * Arel abstraction layer added
53
- * count() queries resulting in "Cannot visit AttrSearchableGrammar::Nodes..." fixed
84
+ * `count()` queries resulting in "Cannot visit AttrSearchableGrammar::Nodes..." fixed
54
85
  * Better error messages
55
- * Model#unsafe_search added
86
+ * `Model#unsafe_search` added
56
87
 
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,18 @@
1
+
2
+ # Contributing
3
+
4
+ There are two ways to contribute: issues and pull requests.
5
+
6
+ ## Issues
7
+
8
+ You are very welcome to submit a github issue in case you find a bug.
9
+ The more detailed, the easier to reproduce. So please be verbose.
10
+
11
+ ## Pull Requests
12
+
13
+ 1. Fork it
14
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
15
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
16
+ 4. Push to the branch (`git push origin my-new-feature`)
17
+ 5. Create new Pull Request
18
+
data/Gemfile CHANGED
@@ -1,23 +1,10 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in search_cop.gemspec
4
4
  gemspec
5
5
 
6
- platforms :jruby do
7
- gem 'activerecord-jdbcmysql-adapter'
8
- gem 'activerecord-jdbcsqlite3-adapter'
9
- gem 'activerecord-jdbcpostgresql-adapter'
10
- end
11
-
12
6
  platforms :ruby do
13
- gem 'sqlite3'
14
- gem 'mysql2'
15
- gem 'pg'
7
+ gem "mysql2"
8
+ gem "pg"
9
+ gem "sqlite3"
16
10
  end
17
-
18
- platforms :rbx do
19
- gem 'racc'
20
- gem 'rubysl', '~> 2.0'
21
- gem 'psych'
22
- end
23
-
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # SearchCop
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/mrkamel/search_cop.png?branch=master)](http://travis-ci.org/mrkamel/search_cop)
3
+ [![Build Status](https://github.com/mrkamel/search_cop/workflows/test/badge.svg?branch=master)](https://github.com/mrkamel/search_cop/actions?query=workflow%3Atest)
4
4
  [![Code Climate](https://codeclimate.com/github/mrkamel/search_cop.png)](https://codeclimate.com/github/mrkamel/search_cop)
5
- [![Dependency Status](https://gemnasium.com/mrkamel/search_cop.png?travis)](https://gemnasium.com/mrkamel/search_cop)
6
5
  [![Gem Version](https://badge.fury.io/rb/search_cop.svg)](http://badge.fury.io/rb/search_cop)
7
6
 
8
7
  ![search_cop](https://raw.githubusercontent.com/mrkamel/search_cop_logo/master/search_cop.png)
@@ -29,24 +28,17 @@ to make optimal use of them. Read more below.
29
28
  Complex hash-based queries are supported as well:
30
29
 
31
30
  ```ruby
32
- Book.search(:author => "Rowling", :title => "Harry Potter")
33
- Book.search(:or => [{:author => "Rowling"}, {:author => "Tolkien"}])
34
- Book.search(:and => [{:price => {:gt => 10}}, {:not => {:stock => 0}}, :or => [{:title => "Potter"}, {:author => "Rowling"}]])
35
- Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}])
31
+ Book.search(author: "Rowling", title: "Harry Potter")
32
+ Book.search(or: [{author: "Rowling"}, {author: "Tolkien"}])
33
+ Book.search(and: [{price: {gt: 10}}, {not: {stock: 0}}, or: [{title: "Potter"}, {author: "Rowling"}]])
34
+ Book.search(or: [{query: "Rowling -Potter"}, {query: "Tolkien -Rings"}])
35
+ Book.search(title: {my_custom_sql_query: "Rowl"}})
36
36
  # ...
37
37
  ```
38
38
 
39
- ## AttrSearchable is now SearchCop
40
-
41
- As the set of features of AttrSearchable grew and grew, it has been neccessary
42
- to change its DSL and name, as no `attr_searchable` method is present anymore.
43
- The new DSL is cleaner and more concise. Morever, the migration process is
44
- simple. Please take a look into the migration guide
45
- [MIGRATION.md](https://github.com/mrkamel/search_cop/blob/master/MIGRATION.md)
46
-
47
39
  ## Installation
48
40
 
49
- For Rails/ActiveRecord 3 (or 4), add this line to your application's Gemfile:
41
+ Add this line to your application's Gemfile:
50
42
 
51
43
  gem 'search_cop'
52
44
 
@@ -69,8 +61,8 @@ class Book < ActiveRecord::Base
69
61
 
70
62
  search_scope :search do
71
63
  attributes :title, :description, :stock, :price, :created_at, :available
72
- attributes :comment => ["comments.title", "comments.message"]
73
- attributes :author => "author.name"
64
+ attributes comment: ["comments.title", "comments.message"]
65
+ attributes author: "author.name"
74
66
  # ...
75
67
  end
76
68
 
@@ -122,9 +114,27 @@ As `Book.search(...)` returns an `ActiveRecord::Relation`, you are free to pre-
122
114
  or post-process the search results in every possible way:
123
115
 
124
116
  ```ruby
125
- Book.where(:available => true).search("Harry Potter").order("books.id desc").paginate(:page => params[:page])
117
+ Book.where(available: true).search("Harry Potter").order("books.id desc").paginate(page: params[:page])
126
118
  ```
127
119
 
120
+ ## Security
121
+
122
+ When you pass a query string to SearchCop, it gets parsed, analyzed and mapped
123
+ to finally build up an SQL query. To be more precise, when SearchCop parses the
124
+ query, it creates objects (nodes), which represent the query expressions (And-,
125
+ Or-, Not-, String-, Date-, etc Nodes). To build the SQL query, SearchCop uses
126
+ the concept of visitors like e.g. used in
127
+ [Arel](https://github.com/rails/arel), such that, for every node there must be
128
+ a [visitor](https://github.com/mrkamel/search_cop/blob/master/lib/search_cop/visitors/visitor.rb),
129
+ which transforms the node to SQL. When there is no visitor, an exception is
130
+ raised when the query builder tries to "visit" the node. The visitors are
131
+ responsible for sanitizing the user supplied input. This is primilarly done via
132
+ quoting (string-, table-name-, column-quoting, etc). SearchCop is using the
133
+ methods provided by the ActiveRecord connection adapter for sanitizing/quoting
134
+ to prevent SQL injection. While we can never be 100% safe from security issues,
135
+ SearchCop takes security issues seriously. Please report responsibly via
136
+ security at flakks dot com in case you find any security related issues.
137
+
128
138
  ## Fulltext index capabilities
129
139
 
130
140
  By default, i.e. if you don't tell SearchCop about your fulltext indices,
@@ -172,12 +182,12 @@ search in, such that SearchCop must no longer search within all fields:
172
182
 
173
183
  ```ruby
174
184
  search_scope :search do
175
- attributes :all => [:author, :title]
185
+ attributes all: [:author, :title]
176
186
 
177
- options :all, :type => :fulltext, :default => true
187
+ options :all, :type => :fulltext, default: true
178
188
 
179
- # Use :default => true to explicitly enable fields as default fields (whitelist approach)
180
- # Use :default => false to explicitly disable fields as default fields (blacklist approach)
189
+ # Use default: true to explicitly enable fields as default fields (whitelist approach)
190
+ # Use default: false to explicitly disable fields as default fields (blacklist approach)
181
191
  end
182
192
  ```
183
193
 
@@ -255,6 +265,23 @@ Regarding compound indices for PostgreSQL, use:
255
265
  ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
256
266
  ```
257
267
 
268
+ To handle NULL values with PostgreSQL correctly, use COALESCE both at index
269
+ creation time and when specifying the `search_scope`:
270
+
271
+ ```ruby
272
+ ActiveRecord::Base.connection.execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', COALESCE(author, '') || ' ' || COALESCE(title, '')))"
273
+ ```
274
+
275
+ plus:
276
+
277
+ ```ruby
278
+ search_scope :search do
279
+ attributes :title
280
+
281
+ options :title, :type => :fulltext, coalesce: true
282
+ end
283
+ ```
284
+
258
285
  To use another PostgreSQL dictionary than `simple`, you have to create the
259
286
  index accordingly and you need tell SearchCop about it, e.g.:
260
287
 
@@ -262,7 +289,7 @@ index accordingly and you need tell SearchCop about it, e.g.:
262
289
  search_scope :search do
263
290
  attributes :title
264
291
 
265
- options :title, :type => :fulltext, :dictionary => "english"
292
+ options :title, :type => :fulltext, dictionary: "english"
266
293
  end
267
294
  ```
268
295
 
@@ -273,7 +300,7 @@ For more details about PostgreSQL fulltext indices visit
273
300
 
274
301
  In case you expose non-fulltext attributes to search queries (price, stock,
275
302
  etc.), the respective queries, like `Book.search("stock > 0")`, will profit
276
- from from the usual non-fulltext indices. Thus, you should add a usual index on
303
+ from the usual non-fulltext indices. Thus, you should add a usual index on
277
304
  every column you expose to search queries plus a fulltext index for every
278
305
  fulltext attribute.
279
306
 
@@ -289,7 +316,7 @@ class User < ActiveRecord::Base
289
316
  search_scope :search do
290
317
  attributes :username
291
318
 
292
- options :username, :left_wildcard => false
319
+ options :username, left_wildcard: false
293
320
  end
294
321
 
295
322
  # ...
@@ -302,6 +329,51 @@ User.search("admin")
302
329
  # ... WHERE users.username LIKE 'admin%'
303
330
  ```
304
331
 
332
+ Similarly, you can disable the right wildcard as well:
333
+
334
+ ```ruby
335
+ search_scope :search do
336
+ attributes :username
337
+
338
+ options :username, right_wildcard: false
339
+ end
340
+ ```
341
+
342
+ ## Default operator
343
+
344
+ When you define multiple fields on a search scope, SearcCop will use
345
+ by default the AND operator to concatenate the conditions, e.g:
346
+
347
+ ```ruby
348
+ class User < ActiveRecord::Base
349
+ include SearchCop
350
+
351
+ search_scope :search do
352
+ attributes :username, :fullname
353
+ end
354
+
355
+ # ...
356
+ end
357
+ ```
358
+
359
+ So a search like `User.search("something")` will generate a query
360
+ with the following conditions:
361
+
362
+ ```sql
363
+ ... WHERE username LIKE '%something%' AND fullname LIKE '%something%'
364
+ ```
365
+
366
+ However, there are cases where using AND as the default operator is not desired,
367
+ so SearchCop allows you to override it and use OR as the default operator instead.
368
+ A query like `User.search("something", default_operator: :or)` will
369
+ generate the query using OR to concatenate the conditions
370
+
371
+ ```sql
372
+ ... WHERE username LIKE '%something%' OR fullname LIKE '%something%'
373
+ ```
374
+
375
+ Finally, please note that you can apply it to fulltext indices/queries as well.
376
+
305
377
  ## Associations
306
378
 
307
379
  If you specify searchable attributes from another model, like
@@ -313,7 +385,7 @@ class Book < ActiveRecord::Base
313
385
  belongs_to :author
314
386
 
315
387
  search_scope :search do
316
- attributes :author => "author.name"
388
+ attributes author: "author.name"
317
389
  end
318
390
 
319
391
  # ...
@@ -347,13 +419,13 @@ class Book < ActiveRecord::Base
347
419
  # ...
348
420
 
349
421
  search_scope :search do
350
- attributes :similar => ["similar_books.title", "similar_books.description"]
422
+ attributes similar: ["similar_books.title", "similar_books.description"]
351
423
 
352
424
  scope do
353
425
  joins "left outer join books similar_books on ..."
354
426
  end
355
427
 
356
- aliases :similar_books => Book # Tell SearchCop how to map SQL aliases to models
428
+ aliases similar_books: Book # Tell SearchCop how to map SQL aliases to models
357
429
  end
358
430
 
359
431
  # ...
@@ -370,7 +442,7 @@ class Book < ActiveRecord::Base
370
442
  has_many :users, :through => :comments
371
443
 
372
444
  search_scope :search do
373
- attributes :user => "users.username"
445
+ attributes user: "users.username"
374
446
  end
375
447
 
376
448
  # ...
@@ -393,7 +465,7 @@ class Book < ActiveRecord::Base
393
465
  belongs_to :user
394
466
 
395
467
  search_scope :search do
396
- attributes :user => ["user.username", "users_books.username"]
468
+ attributes user: ["user.username", "users_books.username"]
397
469
  end
398
470
 
399
471
  # ...
@@ -431,12 +503,48 @@ Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
431
503
  and `matches`, `OR` has precedence over `AND`. `NOT` can only be used as infix
432
504
  operator regarding a single attribute.
433
505
 
434
- Hash based queries support `:and => [...]` and `:or => [...]`, which take an array
435
- of `:not => {...}`, `:matches => {...}`, `:eq => {...}`, `:not_eq => {...}`,
436
- `:lt => {...}`, `:lteq => {...}`, `:gt => {...}`, `:gteq => {...}` and `:query => "..."`
437
- arguments. Moreover, `:query => "..."` makes it possible to create sub-queries.
506
+ Hash based queries support `and: [...]` and `or: [...]`, which take an array
507
+ of `not: {...}`, `matches: {...}`, `eq: {...}`, `not_eq: {...}`,
508
+ `lt: {...}`, `lteq: {...}`, `gt: {...}`, `gteq: {...}` and `query: "..."`
509
+ arguments. Moreover, `query: "..."` makes it possible to create sub-queries.
438
510
  The other rules for query string queries apply to hash based queries as well.
439
511
 
512
+ ### Custom operators (Hash based queries)
513
+
514
+ SearchCop also provides the ability to define custom operators by defining a
515
+ `generator` in `search_scope`. They can then be used with the hash based query
516
+ search. This is useful when you want to use database operators that are not
517
+ supported by SearchCop.
518
+
519
+ Please note, when using generators, you are responsible for sanitizing/quoting
520
+ the values (see example below). Otherwise your generator will allow SQL
521
+ injection. Thus, please only use generators if you know what you're doing.
522
+
523
+ For example, if you wanted to perform a `LIKE` query where a book title starts
524
+ with a string, you can define the search scope like so:
525
+
526
+ ```ruby
527
+ search_scope :search do
528
+ attributes :title
529
+
530
+ generator :starts_with do |column_name, raw_value|
531
+ pattern = "#{raw_value}%"
532
+ "#{column_name} LIKE #{quote pattern}"
533
+ end
534
+ end
535
+ ```
536
+
537
+ When you want to perform the search you use it like this:
538
+
539
+ ```ruby
540
+ Book.search(title: { starts_with: "The Great" })
541
+ ```
542
+
543
+ Security Note: The query returned from the generator will be interpolated
544
+ directly into the query that goes to your database. This opens up a potential
545
+ SQL Injection point in your app. If you use this feature you'll want to make
546
+ sure the query you're returning is safe to execute.
547
+
440
548
  ## Mapping
441
549
 
442
550
  When searching in boolean, datetime, timestamp, etc. fields, SearchCop
@@ -521,7 +629,7 @@ class Product < ActiveRecord::Base
521
629
  search_scope :search do
522
630
  attributes :title, :description
523
631
 
524
- options :title, :default => true
632
+ options :title, default: true
525
633
  end
526
634
  end
527
635