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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +42 -0
- data/.rubocop.yml +128 -0
- data/CHANGELOG.md +36 -5
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +4 -17
- data/README.md +143 -35
- data/Rakefile +0 -1
- data/docker-compose.yml +18 -0
- data/gemfiles/rails5.gemfile +13 -0
- data/gemfiles/rails6.gemfile +13 -0
- data/lib/search_cop.rb +15 -13
- data/lib/search_cop/grammar_parser.rb +3 -4
- data/lib/search_cop/hash_parser.rb +23 -17
- data/lib/search_cop/helpers.rb +15 -0
- data/lib/search_cop/query_builder.rb +2 -4
- data/lib/search_cop/query_info.rb +0 -2
- data/lib/search_cop/search_scope.rb +8 -5
- data/lib/search_cop/version.rb +1 -1
- data/lib/search_cop/visitors.rb +0 -2
- data/lib/search_cop/visitors/mysql.rb +9 -7
- data/lib/search_cop/visitors/postgres.rb +18 -8
- data/lib/search_cop/visitors/visitor.rb +13 -6
- data/lib/search_cop_grammar.rb +18 -9
- data/lib/search_cop_grammar.treetop +6 -4
- data/lib/search_cop_grammar/attributes.rb +77 -34
- data/lib/search_cop_grammar/nodes.rb +7 -2
- data/search_cop.gemspec +9 -10
- data/test/and_test.rb +6 -8
- data/test/boolean_test.rb +7 -9
- data/test/database.yml +4 -1
- data/test/date_test.rb +38 -12
- data/test/datetime_test.rb +45 -12
- data/test/default_operator_test.rb +51 -0
- data/test/error_test.rb +2 -4
- data/test/float_test.rb +16 -11
- data/test/fulltext_test.rb +8 -10
- data/test/hash_test.rb +39 -31
- data/test/integer_test.rb +9 -11
- data/test/not_test.rb +6 -8
- data/test/or_test.rb +8 -10
- data/test/scope_test.rb +11 -13
- data/test/search_cop_test.rb +36 -34
- data/test/string_test.rb +67 -19
- data/test/test_helper.rb +24 -18
- data/test/visitor_test.rb +15 -8
- metadata +41 -55
- data/.travis.yml +0 -34
- data/Appraisals +0 -20
- data/gemfiles/3.2.gemfile +0 -26
- data/gemfiles/4.0.gemfile +0 -26
- data/gemfiles/4.1.gemfile +0 -26
- data/gemfiles/4.2.gemfile +0 -26
data/lib/search_cop_grammar.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
-
|
2
1
|
require "search_cop_grammar/attributes"
|
3
2
|
require "search_cop_grammar/nodes"
|
4
3
|
|
5
4
|
module SearchCopGrammar
|
6
5
|
class BaseNode < Treetop::Runtime::SyntaxNode
|
7
|
-
|
6
|
+
attr_writer :query_info, :query_options
|
8
7
|
|
9
8
|
def query_info
|
10
|
-
@query_info || parent.query_info
|
9
|
+
(@query_info ||= nil) || parent.query_info
|
10
|
+
end
|
11
|
+
|
12
|
+
def query_options
|
13
|
+
(@query_options ||= nil) || parent.query_options
|
11
14
|
end
|
12
15
|
|
13
16
|
def evaluate
|
@@ -15,7 +18,7 @@ module SearchCopGrammar
|
|
15
18
|
end
|
16
19
|
|
17
20
|
def elements
|
18
|
-
super.
|
21
|
+
super.reject { |element| element.instance_of?(Treetop::Runtime::SyntaxNode) }
|
19
22
|
end
|
20
23
|
|
21
24
|
def collection_for(key)
|
@@ -94,13 +97,13 @@ module SearchCopGrammar
|
|
94
97
|
|
95
98
|
class SingleQuotedAnywhereExpression < AnywhereExpression
|
96
99
|
def text_value
|
97
|
-
super.gsub
|
100
|
+
super.gsub(/^'|'$/, "")
|
98
101
|
end
|
99
102
|
end
|
100
103
|
|
101
104
|
class DoubleQuotedAnywhereExpression < AnywhereExpression
|
102
105
|
def text_value
|
103
|
-
super.gsub
|
106
|
+
super.gsub(/^"|"$/, "")
|
104
107
|
end
|
105
108
|
end
|
106
109
|
|
@@ -110,6 +113,13 @@ module SearchCopGrammar
|
|
110
113
|
end
|
111
114
|
end
|
112
115
|
|
116
|
+
class AndOrExpression < BaseNode
|
117
|
+
def evaluate
|
118
|
+
default_operator = SearchCop::Helpers.sanitize_default_operator(query_options)
|
119
|
+
[elements.first.evaluate, elements.last.evaluate].inject(default_operator)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
113
123
|
class OrExpression < BaseNode
|
114
124
|
def evaluate
|
115
125
|
[elements.first.evaluate, elements.last.evaluate].inject(:or)
|
@@ -130,16 +140,15 @@ module SearchCopGrammar
|
|
130
140
|
|
131
141
|
class SingleQuotedValue < BaseNode
|
132
142
|
def text_value
|
133
|
-
super.gsub
|
143
|
+
super.gsub(/^'|'$/, "")
|
134
144
|
end
|
135
145
|
end
|
136
146
|
|
137
147
|
class DoubleQuotedValue < BaseNode
|
138
148
|
def text_value
|
139
|
-
super.gsub
|
149
|
+
super.gsub(/^"|"$/, "")
|
140
150
|
end
|
141
151
|
end
|
142
152
|
|
143
153
|
class Value < BaseNode; end
|
144
154
|
end
|
145
|
-
|
@@ -9,7 +9,9 @@ grammar SearchCopGrammar
|
|
9
9
|
end
|
10
10
|
|
11
11
|
rule and_expression
|
12
|
-
or_expression
|
12
|
+
or_expression space? ('AND' / 'and') space? complex_expression <AndExpression> /
|
13
|
+
or_expression space !('OR' / 'or') complex_expression <AndOrExpression> /
|
14
|
+
or_expression
|
13
15
|
end
|
14
16
|
|
15
17
|
rule or_expression
|
@@ -37,7 +39,7 @@ grammar SearchCopGrammar
|
|
37
39
|
end
|
38
40
|
|
39
41
|
rule anywhere_expression
|
40
|
-
"'" [^\']* "'" <SingleQuotedAnywhereExpression> / '"' [^\"]* '"' <DoubleQuotedAnywhereExpression> / [
|
42
|
+
"'" [^\']* "'" <SingleQuotedAnywhereExpression> / '"' [^\"]* '"' <DoubleQuotedAnywhereExpression> / [^[:blank:]()]+ <AnywhereExpression>
|
41
43
|
end
|
42
44
|
|
43
45
|
rule simple_column
|
@@ -45,11 +47,11 @@ grammar SearchCopGrammar
|
|
45
47
|
end
|
46
48
|
|
47
49
|
rule value
|
48
|
-
"'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [
|
50
|
+
"'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^[:blank:]()]+ <Value>
|
49
51
|
end
|
50
52
|
|
51
53
|
rule space
|
52
|
-
[
|
54
|
+
[[:blank:]]+
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require "treetop"
|
3
2
|
|
4
3
|
module SearchCopGrammar
|
@@ -6,6 +5,8 @@ module SearchCopGrammar
|
|
6
5
|
class Collection
|
7
6
|
attr_reader :query_info, :key
|
8
7
|
|
8
|
+
INCLUDED_OPERATORS = [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].freeze
|
9
|
+
|
9
10
|
def initialize(query_info, key)
|
10
11
|
raise(SearchCop::UnknownColumn, "Unknown column #{key}") unless query_info.scope.reflection.attributes[key]
|
11
12
|
|
@@ -26,9 +27,15 @@ module SearchCopGrammar
|
|
26
27
|
end
|
27
28
|
|
28
29
|
[:eq, :not_eq, :lt, :lteq, :gt, :gteq].each do |method|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
define_method method do |value|
|
31
|
+
attributes.collect! { |attribute| attribute.send method, value }.inject(:or)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def generator(generator, value)
|
36
|
+
attributes.collect! do |attribute|
|
37
|
+
SearchCopGrammar::Nodes::Generator.new(attribute, generator: generator, value: value)
|
38
|
+
end.inject(:or)
|
32
39
|
end
|
33
40
|
|
34
41
|
def matches(value)
|
@@ -88,6 +95,18 @@ module SearchCopGrammar
|
|
88
95
|
|
89
96
|
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass, alias_for(table), column, options)
|
90
97
|
end
|
98
|
+
|
99
|
+
def generator_for(name)
|
100
|
+
generators[name]
|
101
|
+
end
|
102
|
+
|
103
|
+
def valid_operator?(operator)
|
104
|
+
(INCLUDED_OPERATORS + generators.keys).include?(operator)
|
105
|
+
end
|
106
|
+
|
107
|
+
def generators
|
108
|
+
query_info.scope.reflection.generators
|
109
|
+
end
|
91
110
|
end
|
92
111
|
|
93
112
|
class Base
|
@@ -117,7 +136,7 @@ module SearchCopGrammar
|
|
117
136
|
false
|
118
137
|
end
|
119
138
|
|
120
|
-
{ :
|
139
|
+
{ eq: "Equality", not_eq: "NotEqual", lt: "LessThan", lteq: "LessThanOrEqual", gt: "GreaterThan", gteq: "GreaterThanOrEqual", matches: "Matches" }.each do |method, class_name|
|
121
140
|
define_method method do |value|
|
122
141
|
raise(SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}") unless compatible?(value)
|
123
142
|
|
@@ -126,19 +145,32 @@ module SearchCopGrammar
|
|
126
145
|
end
|
127
146
|
|
128
147
|
def method_missing(name, *args, &block)
|
129
|
-
@attribute.
|
148
|
+
if @attribute.respond_to?(name)
|
149
|
+
@attribute.send(name, *args, &block)
|
150
|
+
else
|
151
|
+
super
|
152
|
+
end
|
130
153
|
end
|
131
154
|
|
132
|
-
def
|
133
|
-
|
155
|
+
def respond_to_missing?(*args)
|
156
|
+
@attribute.respond_to?(*args) || super
|
134
157
|
end
|
135
158
|
end
|
136
159
|
|
137
160
|
class String < Base
|
138
161
|
def matches_value(value)
|
139
|
-
|
162
|
+
res = value.gsub(/[%_\\]/) { |char| "\\#{char}" }
|
163
|
+
|
164
|
+
if value.strip =~ /^\*|\*$/
|
165
|
+
res = res.gsub(/^\*/, "%") if options[:left_wildcard] != false
|
166
|
+
res = res.gsub(/\*$/, "%") if options[:right_wildcard] != false
|
167
|
+
|
168
|
+
return res
|
169
|
+
end
|
140
170
|
|
141
|
-
|
171
|
+
res = "%#{res}" if options[:left_wildcard] != false
|
172
|
+
res = "#{res}%" if options[:right_wildcard] != false
|
173
|
+
res
|
142
174
|
end
|
143
175
|
|
144
176
|
def matches(value)
|
@@ -156,7 +188,7 @@ module SearchCopGrammar
|
|
156
188
|
|
157
189
|
class Float < WithoutMatches
|
158
190
|
def compatible?(value)
|
159
|
-
return true if value.to_s =~
|
191
|
+
return true if value.to_s =~ /^-?[0-9]+(\.[0-9]+)?$/
|
160
192
|
|
161
193
|
false
|
162
194
|
end
|
@@ -176,20 +208,26 @@ module SearchCopGrammar
|
|
176
208
|
|
177
209
|
class Datetime < WithoutMatches
|
178
210
|
def parse(value)
|
179
|
-
return value
|
180
|
-
|
181
|
-
if value =~ /^[0-9]{
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
elsif value =~ /^
|
186
|
-
::Time.new(
|
187
|
-
elsif value
|
211
|
+
return value..value unless value.is_a?(::String)
|
212
|
+
|
213
|
+
if value =~ /^[0-9]+ (hour|day|week|month|year)s{0,1} (ago)$/
|
214
|
+
number, period, ago = value.split(" ")
|
215
|
+
time = number.to_i.send(period.to_sym).send(ago.to_sym)
|
216
|
+
time..::Time.now
|
217
|
+
elsif value =~ /^[0-9]{4}$/
|
218
|
+
::Time.new(value).beginning_of_year..::Time.new(value).end_of_year
|
219
|
+
elsif value =~ %r{^([0-9]{4})(\.|-|/)([0-9]{1,2})$}
|
220
|
+
::Time.new(Regexp.last_match(1), Regexp.last_match(3), 15).beginning_of_month..::Time.new(Regexp.last_match(1), Regexp.last_match(3), 15).end_of_month
|
221
|
+
elsif value =~ %r{^([0-9]{1,2})(\.|-|/)([0-9]{4})$}
|
222
|
+
::Time.new(Regexp.last_match(3), Regexp.last_match(1), 15).beginning_of_month..::Time.new(Regexp.last_match(3), Regexp.last_match(1), 15).end_of_month
|
223
|
+
elsif value =~ %r{^[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}$} || value =~ %r{^[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}$}
|
188
224
|
time = ::Time.parse(value)
|
189
|
-
time.beginning_of_day
|
190
|
-
|
225
|
+
time.beginning_of_day..time.end_of_day
|
226
|
+
elsif value =~ %r{[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}} || value =~ %r{[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}}
|
191
227
|
time = ::Time.parse(value)
|
192
|
-
time
|
228
|
+
time..time
|
229
|
+
else
|
230
|
+
raise ArgumentError
|
193
231
|
end
|
194
232
|
rescue ArgumentError
|
195
233
|
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
@@ -220,17 +258,23 @@ module SearchCopGrammar
|
|
220
258
|
|
221
259
|
class Date < Datetime
|
222
260
|
def parse(value)
|
223
|
-
return value
|
224
|
-
|
225
|
-
if value =~ /^[0-9]{
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
elsif value =~ /^
|
230
|
-
::Date.new(
|
231
|
-
|
261
|
+
return value..value unless value.is_a?(::String)
|
262
|
+
|
263
|
+
if value =~ /^[0-9]+ (day|week|month|year)s{0,1} (ago)$/
|
264
|
+
number, period, ago = value.split(" ")
|
265
|
+
time = number.to_i.send(period.to_sym).send(ago.to_sym)
|
266
|
+
time.to_date..::Date.today
|
267
|
+
elsif value =~ /^[0-9]{4}$/
|
268
|
+
::Date.new(value.to_i).beginning_of_year..::Date.new(value.to_i).end_of_year
|
269
|
+
elsif value =~ %r{^([0-9]{4})(\.|-|/)([0-9]{1,2})$}
|
270
|
+
::Date.new(Regexp.last_match(1).to_i, Regexp.last_match(3).to_i, 15).beginning_of_month..::Date.new(Regexp.last_match(1).to_i, Regexp.last_match(3).to_i, 15).end_of_month
|
271
|
+
elsif value =~ %r{^([0-9]{1,2})(\.|-|/)([0-9]{4})$}
|
272
|
+
::Date.new(Regexp.last_match(3).to_i, Regexp.last_match(1).to_i, 15).beginning_of_month..::Date.new(Regexp.last_match(3).to_i, Regexp.last_match(1).to_i, 15).end_of_month
|
273
|
+
elsif value =~ %r{[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}} || value =~ %r{[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}}
|
232
274
|
date = ::Date.parse(value)
|
233
|
-
date
|
275
|
+
date..date
|
276
|
+
else
|
277
|
+
raise ArgumentError
|
234
278
|
end
|
235
279
|
rescue ArgumentError
|
236
280
|
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
@@ -249,4 +293,3 @@ module SearchCopGrammar
|
|
249
293
|
end
|
250
294
|
end
|
251
295
|
end
|
252
|
-
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require "treetop"
|
3
2
|
|
4
3
|
module SearchCopGrammar
|
@@ -73,6 +72,7 @@ module SearchCopGrammar
|
|
73
72
|
class LessThan < Binary; end
|
74
73
|
class LessThanOrEqual < Binary; end
|
75
74
|
class Matches < Binary; end
|
75
|
+
class Generator < Binary; end
|
76
76
|
|
77
77
|
class Not
|
78
78
|
include Base
|
@@ -82,6 +82,12 @@ module SearchCopGrammar
|
|
82
82
|
def initialize(object)
|
83
83
|
@object = object
|
84
84
|
end
|
85
|
+
|
86
|
+
def finalize!
|
87
|
+
@object.finalize!
|
88
|
+
|
89
|
+
self
|
90
|
+
end
|
85
91
|
end
|
86
92
|
|
87
93
|
class MatchesFulltext < Binary
|
@@ -180,4 +186,3 @@ module SearchCopGrammar
|
|
180
186
|
end
|
181
187
|
end
|
182
188
|
end
|
183
|
-
|
data/search_cop.gemspec
CHANGED
@@ -1,29 +1,28 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
3
|
+
require "search_cop/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
6
|
spec.name = "search_cop"
|
8
7
|
spec.version = SearchCop::VERSION
|
9
8
|
spec.authors = ["Benjamin Vetter"]
|
10
9
|
spec.email = ["vetter@flakks.com"]
|
11
|
-
spec.description =
|
12
|
-
spec.summary =
|
10
|
+
spec.description = "Search engine like fulltext query support for ActiveRecord"
|
11
|
+
spec.summary = "Easily perform complex search engine like fulltext queries on your ActiveRecord models"
|
13
12
|
spec.homepage = "https://github.com/mrkamel/search_cop"
|
14
13
|
spec.license = "MIT"
|
15
14
|
|
16
|
-
spec.files = `git ls-files`.split(
|
15
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
16
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
18
|
spec.require_paths = ["lib"]
|
20
19
|
|
21
20
|
spec.add_dependency "treetop"
|
22
21
|
|
23
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
-
spec.add_development_dependency "rake"
|
25
22
|
spec.add_development_dependency "activerecord", ">= 3.0.0"
|
26
|
-
spec.add_development_dependency "
|
27
|
-
spec.add_development_dependency "
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "factory_bot"
|
28
25
|
spec.add_development_dependency "minitest"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rubocop"
|
29
28
|
end
|
data/test/and_test.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
|
2
|
-
require File.expand_path("../test_helper", __FILE__)
|
1
|
+
require File.expand_path("test_helper", __dir__)
|
3
2
|
|
4
3
|
class AndTest < SearchCop::TestCase
|
5
4
|
def test_and_string
|
6
|
-
expected = create(:product, :
|
7
|
-
rejected = create(:product, :
|
5
|
+
expected = create(:product, title: "expected title", description: "Description")
|
6
|
+
rejected = create(:product, title: "Rejected title", description: "Description")
|
8
7
|
|
9
8
|
results = Product.search("title: 'Expected title' description: Description")
|
10
9
|
|
@@ -15,13 +14,12 @@ class AndTest < SearchCop::TestCase
|
|
15
14
|
end
|
16
15
|
|
17
16
|
def test_and_hash
|
18
|
-
expected = create(:product, :
|
19
|
-
rejected = create(:product, :
|
17
|
+
expected = create(:product, title: "Expected title", description: "Description")
|
18
|
+
rejected = create(:product, title: "Rejected title", description: "Description")
|
20
19
|
|
21
|
-
results = Product.search(:
|
20
|
+
results = Product.search(and: [{ title: "Expected title" }, { description: "Description" }])
|
22
21
|
|
23
22
|
assert_includes results, expected
|
24
23
|
refute_includes results, rejected
|
25
24
|
end
|
26
25
|
end
|
27
|
-
|
data/test/boolean_test.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
|
-
|
2
|
-
require File.expand_path("../test_helper", __FILE__)
|
1
|
+
require File.expand_path("test_helper", __dir__)
|
3
2
|
|
4
3
|
class BooleanTest < SearchCop::TestCase
|
5
4
|
def test_mapping
|
6
|
-
product = create(:product, :
|
5
|
+
product = create(:product, available: true)
|
7
6
|
|
8
7
|
assert_includes Product.search("available: 1"), product
|
9
8
|
assert_includes Product.search("available: true"), product
|
10
9
|
assert_includes Product.search("available: yes"), product
|
11
10
|
|
12
|
-
product = create(:product, :
|
11
|
+
product = create(:product, available: false)
|
13
12
|
|
14
13
|
assert_includes Product.search("available: 0"), product
|
15
14
|
assert_includes Product.search("available: false"), product
|
@@ -17,28 +16,28 @@ class BooleanTest < SearchCop::TestCase
|
|
17
16
|
end
|
18
17
|
|
19
18
|
def test_anywhere
|
20
|
-
product = create(:product, :
|
19
|
+
product = create(:product, available: true)
|
21
20
|
|
22
21
|
assert_includes Product.search("true"), product
|
23
22
|
refute_includes Product.search("false"), product
|
24
23
|
end
|
25
24
|
|
26
25
|
def test_includes
|
27
|
-
product = create(:product, :
|
26
|
+
product = create(:product, available: true)
|
28
27
|
|
29
28
|
assert_includes Product.search("available: true"), product
|
30
29
|
refute_includes Product.search("available: false"), product
|
31
30
|
end
|
32
31
|
|
33
32
|
def test_equals
|
34
|
-
product = create(:product, :
|
33
|
+
product = create(:product, available: true)
|
35
34
|
|
36
35
|
assert_includes Product.search("available = true"), product
|
37
36
|
refute_includes Product.search("available = false"), product
|
38
37
|
end
|
39
38
|
|
40
39
|
def test_equals_not
|
41
|
-
product = create(:product, :
|
40
|
+
product = create(:product, available: false)
|
42
41
|
|
43
42
|
assert_includes Product.search("available != true"), product
|
44
43
|
refute_includes Product.search("available != false"), product
|
@@ -50,4 +49,3 @@ class BooleanTest < SearchCop::TestCase
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
end
|
53
|
-
|