search_cop 1.0.6 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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