scoped_search 4.1.12 → 4.3.1
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 +4 -4
- data/CHANGELOG.rdoc +10 -0
- data/lib/scoped_search/auto_complete_builder.rb +12 -3
- data/lib/scoped_search/definition.rb +1 -1
- data/lib/scoped_search/query_builder.rb +24 -4
- data/lib/scoped_search/query_language/parser.rb +29 -7
- data/lib/scoped_search/query_language/tokenizer.rb +1 -1
- data/lib/scoped_search/version.rb +1 -1
- data/lib/scoped_search.rb +1 -0
- data/scoped_search.gemspec +7 -0
- data/spec/integration/auto_complete_spec.rb +62 -3
- data/spec/integration/relation_querying_spec.rb +53 -53
- data/spec/integration/uuid_query_spec.rb +2 -1
- data/spec/unit/parser_spec.rb +21 -1
- data/spec/unit/query_builder_spec.rb +61 -0
- metadata +73 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2413a0e2075ccff25188af0a579df4b8cfde593f38ab801b99bd7379b2344c8b
|
|
4
|
+
data.tar.gz: edc1ec5d32cc2b410fdf9938e49aca0cfba46807850491c3ee6bba28642dfc5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a31a5e76daac85ca7b36cff7fdf38a377258401f38da13fb789afb1d6ca55e01331ce88c78136e7bc4efc64704e59460de267b50bfd8dc47e479c881e2b84b7
|
|
7
|
+
data.tar.gz: 87d273283f63fbc83afb0ab29f163a5625f0fc074c551c5220f004eb902c43a407973bf5470395c90442666ff14e1a69b1b0e7b8aba653c997d180da969429ee
|
data/CHANGELOG.rdoc
CHANGED
|
@@ -6,6 +6,16 @@ Please add an entry to the "Unreleased changes" section in your pull requests.
|
|
|
6
6
|
|
|
7
7
|
=== Unreleased changes
|
|
8
8
|
|
|
9
|
+
=== Version 4.3.1
|
|
10
|
+
- Autocomplete now offers only relevant options available through scopes set on associations (#233)
|
|
11
|
+
|
|
12
|
+
=== Version 4.3.0
|
|
13
|
+
|
|
14
|
+
- Prevent scoped_search from modifying an empty string on newer rubies (#229)
|
|
15
|
+
- Autocomplete now wraps all strings in quotes, not just those containing whitespace (#230)
|
|
16
|
+
- UUID search is now case insensitive (#232)
|
|
17
|
+
- Autocomplete now honors scopes set on associations (#231)
|
|
18
|
+
|
|
9
19
|
=== Version 4.1.10
|
|
10
20
|
|
|
11
21
|
- Fix querying through associations in Rails 6.1 (undefined method join_keys) (#201)
|
|
@@ -207,6 +207,7 @@ module ScopedSearch
|
|
|
207
207
|
|
|
208
208
|
def complete_value_from_db(field, special_values, val)
|
|
209
209
|
count = 20 - special_values.count
|
|
210
|
+
|
|
210
211
|
completer_scope(field)
|
|
211
212
|
.where(@options[:value_filter])
|
|
212
213
|
.where(value_conditions(field.quoted_field, val))
|
|
@@ -215,12 +216,20 @@ module ScopedSearch
|
|
|
215
216
|
.distinct
|
|
216
217
|
.map(&field.field)
|
|
217
218
|
.compact
|
|
218
|
-
.map { |v| v.
|
|
219
|
+
.map { |v| v.is_a?(String) ? "\"#{v.gsub('"', '\"')}\"" : v }
|
|
219
220
|
end
|
|
220
221
|
|
|
221
222
|
def completer_scope(field)
|
|
222
|
-
|
|
223
|
-
scope =
|
|
223
|
+
scope = field.klass
|
|
224
|
+
scope = scope.completer_scope(@options) if scope.respond_to?(:completer_scope)
|
|
225
|
+
|
|
226
|
+
if field.klass != field.definition.klass
|
|
227
|
+
reflection = field.definition.reflection_by_name(field.definition.klass, field.relation)
|
|
228
|
+
sub_scope = reflection.active_record.joins(reflection.name)
|
|
229
|
+
sub_scope = sub_scope.select("#{field.klass.table_name}.#{field.klass.primary_key}")
|
|
230
|
+
scope = scope.where(field.klass.primary_key => sub_scope)
|
|
231
|
+
end
|
|
232
|
+
|
|
224
233
|
scope.respond_to?(:reorder) ? scope.reorder(Arel.sql(field.quoted_field)) : scope.scoped(:order => field.quoted_field)
|
|
225
234
|
end
|
|
226
235
|
|
|
@@ -260,7 +260,7 @@ module ScopedSearch
|
|
|
260
260
|
|
|
261
261
|
NUMERICAL_REGXP = /^\-?\d+(\.\d+)?$/
|
|
262
262
|
INTEGER_REGXP = /^\-?\d+$/
|
|
263
|
-
UUID_REGXP = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/
|
|
263
|
+
UUID_REGXP = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i
|
|
264
264
|
|
|
265
265
|
# Returns a list of appropriate fields to search in given a search keyword and operator.
|
|
266
266
|
def default_fields_for(value, operator = nil)
|
|
@@ -146,8 +146,22 @@ module ScopedSearch
|
|
|
146
146
|
# but the field is of datetime type. Change the comparison to return
|
|
147
147
|
# more logical results.
|
|
148
148
|
if field.datetime?
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
if value =~ time_unit_regex("minutes?|hours?")
|
|
150
|
+
span = 1.minute
|
|
151
|
+
elsif value =~ time_unit_regex("days?|weeks?|months?|years?") || value =~ /\b(today|tomorrow|yesterday)\b/i
|
|
152
|
+
span = 1.day
|
|
153
|
+
else
|
|
154
|
+
tokens = DateTime._parse(value)
|
|
155
|
+
# find the smallest unit of time given in input and determine span for further adjustment of the search query
|
|
156
|
+
span = {
|
|
157
|
+
sec: 1.second,
|
|
158
|
+
min: 1.minute,
|
|
159
|
+
hour: 1.hour,
|
|
160
|
+
mday: 1.day,
|
|
161
|
+
mon: 1.month
|
|
162
|
+
}.find { |key, _| tokens[key] }&.last || 1.year
|
|
163
|
+
end
|
|
164
|
+
|
|
151
165
|
if [:eq, :ne].include?(operator)
|
|
152
166
|
# Instead of looking for an exact (non-)match, look for dates that
|
|
153
167
|
# fall inside/outside the range of timestamps of that day.
|
|
@@ -155,13 +169,13 @@ module ScopedSearch
|
|
|
155
169
|
field_sql = field.to_sql(operator, &block)
|
|
156
170
|
return ["#{negate}(#{field_sql} >= ? AND #{field_sql} < ?)", timestamp, timestamp + span]
|
|
157
171
|
|
|
158
|
-
elsif operator == :gt
|
|
172
|
+
elsif span >= 1.day && operator == :gt
|
|
159
173
|
# Make sure timestamps on the given date are not included in the results
|
|
160
174
|
# by moving the date to the next day.
|
|
161
175
|
timestamp += span
|
|
162
176
|
operator = :gte
|
|
163
177
|
|
|
164
|
-
elsif operator == :lte
|
|
178
|
+
elsif span >= 1.day && operator == :lte
|
|
165
179
|
# Make sure the timestamps of the given date are included by moving the
|
|
166
180
|
# date to the next date.
|
|
167
181
|
timestamp += span
|
|
@@ -320,6 +334,12 @@ module ScopedSearch
|
|
|
320
334
|
definition.reflection_by_name(reflection.klass, as).options[:polymorphic]
|
|
321
335
|
end
|
|
322
336
|
|
|
337
|
+
private
|
|
338
|
+
|
|
339
|
+
def time_unit_regex(time_unit)
|
|
340
|
+
/\A\s*\d+\s+\b(?:#{time_unit})\b\s+\b(ago|from\s+now)\b\s*\z/i
|
|
341
|
+
end
|
|
342
|
+
|
|
323
343
|
# This module gets included into the Field class to add SQL generation.
|
|
324
344
|
module Field
|
|
325
345
|
|
|
@@ -31,7 +31,7 @@ module ScopedSearch::QueryLanguage::Parser
|
|
|
31
31
|
next_token if !root_node && peek_token == :lparen # skip starting :lparen
|
|
32
32
|
expressions << parse_logical_expression until peek_token.nil? || peek_token == :rparen
|
|
33
33
|
next_token if !root_node && peek_token == :rparen # skip final :rparen
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
return ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(DEFAULT_SEQUENCE_OPERATOR, expressions, root_node)
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -42,18 +42,23 @@ module ScopedSearch::QueryLanguage::Parser
|
|
|
42
42
|
when :lparen; parse_expression_sequence
|
|
43
43
|
when :not; parse_logical_not_expression
|
|
44
44
|
when :null, :notnull; parse_null_expression
|
|
45
|
+
when *LOGICAL_INFIX_OPERATORS; parse_logical_infix_expression
|
|
45
46
|
else; parse_comparison
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
if LOGICAL_INFIX_OPERATORS.include?(peek_token)
|
|
49
|
-
|
|
50
|
-
rhs = parse_logical_expression
|
|
51
|
-
ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(operator, [lhs, rhs])
|
|
50
|
+
parse_logical_infix_expression([lhs])
|
|
52
51
|
else
|
|
53
52
|
lhs
|
|
54
53
|
end
|
|
55
54
|
end
|
|
56
55
|
|
|
56
|
+
def parse_logical_infix_expression(previous = [])
|
|
57
|
+
operator = next_token
|
|
58
|
+
rhs = parse_logical_expression
|
|
59
|
+
ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(operator, previous + [rhs])
|
|
60
|
+
end
|
|
61
|
+
|
|
57
62
|
# Parses a NOT expression
|
|
58
63
|
def parse_logical_not_expression
|
|
59
64
|
next_token # = skip NOT operator
|
|
@@ -80,7 +85,24 @@ module ScopedSearch::QueryLanguage::Parser
|
|
|
80
85
|
|
|
81
86
|
# Parses a prefix comparison, i.e. without an explicit field: <operator> <value>
|
|
82
87
|
def parse_prefix_comparison
|
|
83
|
-
|
|
88
|
+
token = next_token
|
|
89
|
+
case token
|
|
90
|
+
when :in
|
|
91
|
+
parse_prefix_in(true)
|
|
92
|
+
when :notin
|
|
93
|
+
parse_prefix_in(false)
|
|
94
|
+
else
|
|
95
|
+
ScopedSearch::QueryLanguage::AST::OperatorNode.new(token, [parse_value])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_prefix_in(inclusion)
|
|
100
|
+
cmp, log = inclusion ? [:eq, :or] : [:ne, :and]
|
|
101
|
+
leaves = parse_multiple_values.map do |x|
|
|
102
|
+
leaf = ScopedSearch::QueryLanguage::AST::LeafNode.new(x)
|
|
103
|
+
ScopedSearch::QueryLanguage::AST::OperatorNode.new(cmp, [leaf])
|
|
104
|
+
end
|
|
105
|
+
ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(log, leaves)
|
|
84
106
|
end
|
|
85
107
|
|
|
86
108
|
# Parses an infix expression, i.e. <field> <operator> <value>
|
|
@@ -109,7 +131,7 @@ module ScopedSearch::QueryLanguage::Parser
|
|
|
109
131
|
value = []
|
|
110
132
|
value << current_token if String === next_token until peek_token.nil? || peek_token == :rparen
|
|
111
133
|
next_token if peek_token == :rparen # consume the :rparen
|
|
112
|
-
value
|
|
134
|
+
value
|
|
113
135
|
end
|
|
114
136
|
|
|
115
137
|
# This can either be a constant value or a field name.
|
|
@@ -117,7 +139,7 @@ module ScopedSearch::QueryLanguage::Parser
|
|
|
117
139
|
if String === peek_token
|
|
118
140
|
ScopedSearch::QueryLanguage::AST::LeafNode.new(next_token)
|
|
119
141
|
elsif ([:in, :notin].include? current_token)
|
|
120
|
-
value = parse_multiple_values()
|
|
142
|
+
value = parse_multiple_values().join(',')
|
|
121
143
|
ScopedSearch::QueryLanguage::AST::LeafNode.new(value)
|
|
122
144
|
else
|
|
123
145
|
raise ScopedSearch::QueryNotSupported, "Value expected but found #{peek_token.inspect}"
|
|
@@ -69,7 +69,7 @@ module ScopedSearch::QueryLanguage::Tokenizer
|
|
|
69
69
|
# Tokenizes a keyword that is quoted using double quotes. Allows escaping
|
|
70
70
|
# of double quote characters by backslashes.
|
|
71
71
|
def tokenize_quoted_keyword(&block)
|
|
72
|
-
keyword =
|
|
72
|
+
keyword = String.new
|
|
73
73
|
until next_char.nil? || current_char == '"'
|
|
74
74
|
keyword << (current_char == "\\" ? next_char : current_char)
|
|
75
75
|
end
|
data/lib/scoped_search.rb
CHANGED
data/scoped_search.gemspec
CHANGED
|
@@ -34,6 +34,13 @@ Gem::Specification.new do |gem|
|
|
|
34
34
|
gem.add_development_dependency('rspec', '~> 3.0')
|
|
35
35
|
gem.add_development_dependency('rake')
|
|
36
36
|
|
|
37
|
+
# Rails require these, but don't explicitly depend on them
|
|
38
|
+
gem.add_development_dependency('base64')
|
|
39
|
+
gem.add_development_dependency('benchmark')
|
|
40
|
+
gem.add_development_dependency('bigdecimal')
|
|
41
|
+
gem.add_development_dependency('logger')
|
|
42
|
+
gem.add_development_dependency('mutex_m')
|
|
43
|
+
|
|
37
44
|
gem.rdoc_options << '--title' << gem.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
|
|
38
45
|
gem.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.rdoc', 'CONTRIBUTING.rdoc', 'LICENSE']
|
|
39
46
|
end
|
|
@@ -41,6 +41,20 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
ActiveRecord::Migration.create_table(:bazs, :force => true) do |t|
|
|
45
|
+
t.integer :foo_id
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ActiveRecord::Migration.create_table(:asds, :force => true) do |t|
|
|
49
|
+
t.integer :baz_id
|
|
50
|
+
t.string :string
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ActiveRecord::Migration.create_table(:qwes, :force => true) do |t|
|
|
54
|
+
t.integer :asd_id
|
|
55
|
+
t.string :string
|
|
56
|
+
end
|
|
57
|
+
|
|
44
58
|
class ::Bar < ActiveRecord::Base
|
|
45
59
|
belongs_to :foo
|
|
46
60
|
end
|
|
@@ -65,28 +79,63 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
65
79
|
scoped_search :on => :other_c, :relation => :bars, :rename => 'bars.other_c'.to_sym
|
|
66
80
|
end
|
|
67
81
|
|
|
82
|
+
class ::Baz < ActiveRecord::Base
|
|
83
|
+
belongs_to :foo, -> { where(string: 'foo') }
|
|
84
|
+
has_one :asd, -> { where(string: 'foo') }
|
|
85
|
+
has_one :qwe, through: :asd
|
|
86
|
+
|
|
87
|
+
scoped_search :on => :string, :relation => :foo, :complete_value => true, :rename => 'foos.string'.to_sym
|
|
88
|
+
scoped_search :on => :string, :relation => :asd, :complete_value => true, :rename => 'asds.string'.to_sym
|
|
89
|
+
scoped_search :on => :string, :relation => :qwe, :complete_value => true, :rename => 'qwes.string'.to_sym
|
|
90
|
+
end
|
|
91
|
+
|
|
68
92
|
class ::Infoo < ::Foo
|
|
69
93
|
end
|
|
70
94
|
|
|
95
|
+
class ::Asd < ActiveRecord::Base
|
|
96
|
+
belongs_to :baz
|
|
97
|
+
has_one :qwe
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class ::Qwe < ActiveRecord::Base
|
|
101
|
+
belongs_to :asd
|
|
102
|
+
end
|
|
103
|
+
|
|
71
104
|
@qux_1 = Qux.create!()
|
|
72
105
|
@qux_2 = Qux.create!()
|
|
73
106
|
@foo_1 = Foo.create!(:string => 'foo', :another => 'temp 1', :explicit => 'baz', :int => 9 , :date => 'February 8, 2011' , :unindexed => 10, :qux => @qux_1)
|
|
74
107
|
@foo_2 = Foo.create!(:string => 'foo', :another => 'temp 2', :explicit => 'baz', :int => 10 , :date => 'February 8, 2011' , :unindexed => 10, :qux => @qux_2)
|
|
75
|
-
Foo.create!(:string => 'bar', :another => 'temp "2"', :explicit => 'baz', :int => 22 , :date => 'February 10, 2011', :unindexed => 10)
|
|
108
|
+
@foo_3 = Foo.create!(:string => 'bar', :another => 'temp "2"', :explicit => 'baz', :int => 22 , :date => 'February 10, 2011', :unindexed => 10)
|
|
76
109
|
Foo.create!(:string => 'baz', :another => nil, :explicit => nil , :int => nil, :date => nil , :unindexed => nil)
|
|
77
110
|
20.times { Foo.create!(:explicit => "aaa") }
|
|
78
111
|
|
|
112
|
+
@baz_1 = Baz.create!(:foo => @foo_1)
|
|
113
|
+
@baz_2 = Baz.create!(:foo => @foo_2)
|
|
114
|
+
Baz.create!(:foo => @foo_3)
|
|
115
|
+
|
|
79
116
|
Bar.create!(:related => 'lala', :foo => @foo_1)
|
|
80
117
|
Bar.create!(:related => 'another lala', :foo => @foo_1)
|
|
118
|
+
|
|
119
|
+
@asd_1 = Asd.create!(:string => 'foo', :baz => @baz_1)
|
|
120
|
+
@asd_2 = Asd.create!(:string => 'bar', :baz => @baz_2)
|
|
121
|
+
Qwe.create!(:asd => @asd_1, :string => 'qwe1')
|
|
122
|
+
Qwe.create!(:asd => @asd_2, :string => 'qwe2')
|
|
81
123
|
end
|
|
82
124
|
|
|
83
125
|
after(:all) do
|
|
84
126
|
ActiveRecord::Migration.drop_table(:foos)
|
|
85
127
|
ActiveRecord::Migration.drop_table(:bars)
|
|
128
|
+
ActiveRecord::Migration.drop_table(:bazs)
|
|
129
|
+
ActiveRecord::Migration.drop_table(:asds)
|
|
130
|
+
ActiveRecord::Migration.drop_table(:qwes)
|
|
86
131
|
|
|
87
132
|
Object.send :remove_const, :Foo
|
|
88
133
|
Object.send :remove_const, :Bar
|
|
134
|
+
Object.send :remove_const, :Baz
|
|
135
|
+
Object.send :remove_const, :Qux
|
|
89
136
|
Object.send :remove_const, :Infoo
|
|
137
|
+
Object.send :remove_const, :Asd
|
|
138
|
+
Object.send :remove_const, :Qwe
|
|
90
139
|
|
|
91
140
|
ScopedSearch::RSpec::Database.close_connection
|
|
92
141
|
end
|
|
@@ -169,10 +218,10 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
169
218
|
end
|
|
170
219
|
|
|
171
220
|
it "should complete values should contain baz" do
|
|
172
|
-
Foo.complete_for('explicit = ').should contain('explicit = baz')
|
|
221
|
+
Foo.complete_for('explicit = ').should contain('explicit = "baz"')
|
|
173
222
|
end
|
|
174
223
|
|
|
175
|
-
it "should complete values with quotes where
|
|
224
|
+
it "should complete values with quotes where value is a string" do
|
|
176
225
|
Foo.complete_for('alias = ').should contain('alias = "temp \"2\""')
|
|
177
226
|
end
|
|
178
227
|
end
|
|
@@ -267,5 +316,15 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
267
316
|
Foo.complete_for('int =', value_filter: { qux_id: 99 }).should == []
|
|
268
317
|
end
|
|
269
318
|
end
|
|
319
|
+
|
|
320
|
+
context 'autocompleting with scopes' do
|
|
321
|
+
it 'should honor the scope' do
|
|
322
|
+
Baz.complete_for('foos.string =').should == ['foos.string = "foo"']
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it 'should honor scope on the through relation' do
|
|
326
|
+
Baz.complete_for('qwes.string =').should == ['qwes.string = "qwe1"']
|
|
327
|
+
end
|
|
328
|
+
end
|
|
270
329
|
end
|
|
271
330
|
end
|
|
@@ -249,13 +249,13 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
249
249
|
before do
|
|
250
250
|
|
|
251
251
|
# Create some tables
|
|
252
|
-
ActiveRecord::Migration.create_table(:mars) { |t| t.integer :koo_id; t.integer :
|
|
253
|
-
ActiveRecord::Migration.create_table(:
|
|
252
|
+
ActiveRecord::Migration.create_table(:mars) { |t| t.integer :koo_id; t.integer :zab_id }
|
|
253
|
+
ActiveRecord::Migration.create_table(:zabs) { |t| t.string :related }
|
|
254
254
|
ActiveRecord::Migration.create_table(:koos) { |t| t.string :foo }
|
|
255
255
|
|
|
256
256
|
# The related classes
|
|
257
|
-
class Mar < ActiveRecord::Base; belongs_to :
|
|
258
|
-
class
|
|
257
|
+
class Mar < ActiveRecord::Base; belongs_to :zab; belongs_to :koo; end
|
|
258
|
+
class Zab < ActiveRecord::Base; has_many :mars; end
|
|
259
259
|
|
|
260
260
|
# The class on which to call search_for
|
|
261
261
|
class Koo < ActiveRecord::Base
|
|
@@ -263,38 +263,38 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
263
263
|
# having the source option here is not needed for the statement correctness.
|
|
264
264
|
# It is here to fail the code introduced in 2.6.2 that wrongly detected source instead of source_type
|
|
265
265
|
# as an indication for a polymorphic relation.
|
|
266
|
-
has_many :
|
|
266
|
+
has_many :zabs, :through => :mars, :source => :zab
|
|
267
267
|
|
|
268
|
-
scoped_search :relation => :
|
|
268
|
+
scoped_search :relation => :zabs, :on => :related
|
|
269
269
|
end
|
|
270
270
|
|
|
271
271
|
@koo_1 = Koo.create!(:foo => 'foo')
|
|
272
272
|
@koo_2 = Koo.create!(:foo => 'foo too')
|
|
273
273
|
@koo_3 = Koo.create!(:foo => 'foo three')
|
|
274
274
|
|
|
275
|
-
@
|
|
276
|
-
@
|
|
275
|
+
@zab_1 = Zab.create(:related => 'zab')
|
|
276
|
+
@zab_2 = Zab.create(:related => 'zab too!')
|
|
277
277
|
|
|
278
|
-
@bar_1 = Mar.create!(:koo => @koo_1, :
|
|
278
|
+
@bar_1 = Mar.create!(:koo => @koo_1, :zab => @zab_1)
|
|
279
279
|
@bar_2 = Mar.create!(:koo => @koo_1)
|
|
280
|
-
@bar_3 = Mar.create!(:koo => @koo_2, :
|
|
281
|
-
@bar_3 = Mar.create!(:koo => @koo_2, :
|
|
282
|
-
@bar_3 = Mar.create!(:koo => @koo_2, :
|
|
280
|
+
@bar_3 = Mar.create!(:koo => @koo_2, :zab => @zab_1)
|
|
281
|
+
@bar_3 = Mar.create!(:koo => @koo_2, :zab => @zab_2)
|
|
282
|
+
@bar_3 = Mar.create!(:koo => @koo_2, :zab => @zab_2)
|
|
283
283
|
@bar_4 = Mar.create!(:koo => @koo_3)
|
|
284
284
|
end
|
|
285
285
|
|
|
286
286
|
after do
|
|
287
|
-
ActiveRecord::Migration.drop_table(:
|
|
287
|
+
ActiveRecord::Migration.drop_table(:zabs)
|
|
288
288
|
ActiveRecord::Migration.drop_table(:mars)
|
|
289
289
|
ActiveRecord::Migration.drop_table(:koos)
|
|
290
290
|
end
|
|
291
291
|
|
|
292
|
-
it "should find the two records that are related to a
|
|
293
|
-
Koo.search_for('
|
|
292
|
+
it "should find the two records that are related to a zab record" do
|
|
293
|
+
Koo.search_for('zab').length.should == 2
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
-
it "should find the two records that are related to a
|
|
297
|
-
Koo.search_for('related=
|
|
296
|
+
it "should find the two records that are related to a zab record" do
|
|
297
|
+
Koo.search_for('related=zab AND related="zab too!"').length.should == 1
|
|
298
298
|
end
|
|
299
299
|
end
|
|
300
300
|
|
|
@@ -303,27 +303,27 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
303
303
|
before do
|
|
304
304
|
|
|
305
305
|
# Create some tables
|
|
306
|
-
ActiveRecord::Migration.create_table(:zars) { |t| t.integer :
|
|
307
|
-
ActiveRecord::Migration.create_table(:
|
|
306
|
+
ActiveRecord::Migration.create_table(:zars) { |t| t.integer :zab_id }
|
|
307
|
+
ActiveRecord::Migration.create_table(:zabs) { |t| t.string :related }
|
|
308
308
|
ActiveRecord::Migration.create_table(:zoos) { |t| t.integer :zar_id; t.string :foo }
|
|
309
309
|
|
|
310
310
|
# The related classes
|
|
311
|
-
class Zar < ActiveRecord::Base; belongs_to :
|
|
312
|
-
class
|
|
311
|
+
class Zar < ActiveRecord::Base; belongs_to :zab; has_many :zoos; end
|
|
312
|
+
class Zab < ActiveRecord::Base; has_many :zars; end
|
|
313
313
|
|
|
314
314
|
# The class on which to call search_for
|
|
315
315
|
class Zoo < ActiveRecord::Base
|
|
316
316
|
belongs_to :zar
|
|
317
|
-
has_many :
|
|
317
|
+
has_many :zabs, :through => :zar
|
|
318
318
|
|
|
319
|
-
scoped_search :relation => :
|
|
319
|
+
scoped_search :relation => :zabs, :on => :related
|
|
320
320
|
end
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
zab_1 = Zab.create(:related => 'zab')
|
|
323
|
+
zab_2 = Zab.create(:related => 'zab too!')
|
|
324
324
|
|
|
325
|
-
zar_1 = Zar.create!( :
|
|
326
|
-
zar_2 = Zar.create!( :
|
|
325
|
+
zar_1 = Zar.create!( :zab => zab_1)
|
|
326
|
+
zar_2 = Zar.create!( :zab => zab_2)
|
|
327
327
|
|
|
328
328
|
Zoo.create!(:zar => zar_1, :foo => 'foo')
|
|
329
329
|
Zoo.create!(:zar => zar_1, :foo => 'foo too')
|
|
@@ -331,17 +331,17 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
331
331
|
end
|
|
332
332
|
|
|
333
333
|
after do
|
|
334
|
-
ActiveRecord::Migration.drop_table(:
|
|
334
|
+
ActiveRecord::Migration.drop_table(:zabs)
|
|
335
335
|
ActiveRecord::Migration.drop_table(:zars)
|
|
336
336
|
ActiveRecord::Migration.drop_table(:zoos)
|
|
337
337
|
end
|
|
338
338
|
|
|
339
|
-
it "should find the three records that are related to a
|
|
340
|
-
Zoo.search_for('
|
|
339
|
+
it "should find the three records that are related to a zab record" do
|
|
340
|
+
Zoo.search_for('zab').length.should == 3
|
|
341
341
|
end
|
|
342
342
|
|
|
343
|
-
it "should find no records that are related to a
|
|
344
|
-
Zoo.search_for('related=
|
|
343
|
+
it "should find no records that are related to a zab record" do
|
|
344
|
+
Zoo.search_for('related=zab AND related="zab too!"').length.should == 0
|
|
345
345
|
end
|
|
346
346
|
end
|
|
347
347
|
|
|
@@ -392,8 +392,8 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
392
392
|
@tag_2 = Tag.create!(:foo => 'foo too')
|
|
393
393
|
@tag_3 = Tag.create!(:foo => 'foo three')
|
|
394
394
|
|
|
395
|
-
@dog_1 = Dog.create(:related => '
|
|
396
|
-
@dog_2 = Dog.create(:related => '
|
|
395
|
+
@dog_1 = Dog.create(:related => 'zab')
|
|
396
|
+
@dog_2 = Dog.create(:related => 'zab too!')
|
|
397
397
|
@cat_1 = Cat.create(:related => 'mitzi')
|
|
398
398
|
|
|
399
399
|
@owner_1 = Owner.create(:name => 'Fred', :dogs => [@dog_1])
|
|
@@ -429,11 +429,11 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
429
429
|
end
|
|
430
430
|
|
|
431
431
|
it "should find the two tags that are related to a dog record" do
|
|
432
|
-
Tag.search_for('dog=
|
|
432
|
+
Tag.search_for('dog=zab').length.should == 2
|
|
433
433
|
end
|
|
434
434
|
|
|
435
435
|
it "should find the 3 tags that are related to dogs record" do
|
|
436
|
-
Tag.search_for('
|
|
436
|
+
Tag.search_for('zab').length.should == 3
|
|
437
437
|
end
|
|
438
438
|
end
|
|
439
439
|
|
|
@@ -547,22 +547,22 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
547
547
|
before do
|
|
548
548
|
|
|
549
549
|
# Create some tables with namespaces
|
|
550
|
-
ActiveRecord::Migration.create_table(:zan_mars) { |t| t.integer :koo_id; t.integer :
|
|
551
|
-
ActiveRecord::Migration.create_table(:
|
|
550
|
+
ActiveRecord::Migration.create_table(:zan_mars) { |t| t.integer :koo_id; t.integer :zab_id }
|
|
551
|
+
ActiveRecord::Migration.create_table(:zan_zabs) { |t| t.string :related }
|
|
552
552
|
ActiveRecord::Migration.create_table(:zan_koos) { |t| t.string :foo }
|
|
553
553
|
|
|
554
554
|
# The related classes
|
|
555
|
-
module Zan; class Mar < ActiveRecord::Base; belongs_to :
|
|
556
|
-
module Zan; class
|
|
555
|
+
module Zan; class Mar < ActiveRecord::Base; belongs_to :zab; belongs_to :koo; self.table_name = "zan_mars"; end; end
|
|
556
|
+
module Zan; class Zab < ActiveRecord::Base; has_many :mars; self.table_name = "zan_zabs"; end; end
|
|
557
557
|
|
|
558
558
|
# The class on which to call search_for
|
|
559
559
|
module Zan
|
|
560
560
|
class Koo < ActiveRecord::Base
|
|
561
561
|
has_many :mars, :class_name => "Zan::Mar"
|
|
562
|
-
has_many :
|
|
562
|
+
has_many :zabs, :through => :mars
|
|
563
563
|
self.table_name = "zan_koos"
|
|
564
564
|
|
|
565
|
-
scoped_search :relation => :
|
|
565
|
+
scoped_search :relation => :zabs, :on => :related
|
|
566
566
|
end
|
|
567
567
|
end
|
|
568
568
|
|
|
@@ -570,29 +570,29 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
570
570
|
@koo_2 = Zan::Koo.create!(:foo => 'foo too')
|
|
571
571
|
@koo_3 = Zan::Koo.create!(:foo => 'foo three')
|
|
572
572
|
|
|
573
|
-
@
|
|
574
|
-
@
|
|
573
|
+
@zab_1 = Zan::Zab.create(:related => 'zab')
|
|
574
|
+
@zab_2 = Zan::Zab.create(:related => 'zab too!')
|
|
575
575
|
|
|
576
|
-
@bar_1 = Zan::Mar.create!(:koo => @koo_1, :
|
|
576
|
+
@bar_1 = Zan::Mar.create!(:koo => @koo_1, :zab => @zab_1)
|
|
577
577
|
@bar_2 = Zan::Mar.create!(:koo => @koo_1)
|
|
578
|
-
@bar_3 = Zan::Mar.create!(:koo => @koo_2, :
|
|
579
|
-
@bar_3 = Zan::Mar.create!(:koo => @koo_2, :
|
|
580
|
-
@bar_3 = Zan::Mar.create!(:koo => @koo_2, :
|
|
578
|
+
@bar_3 = Zan::Mar.create!(:koo => @koo_2, :zab => @zab_1)
|
|
579
|
+
@bar_3 = Zan::Mar.create!(:koo => @koo_2, :zab => @zab_2)
|
|
580
|
+
@bar_3 = Zan::Mar.create!(:koo => @koo_2, :zab => @zab_2)
|
|
581
581
|
@bar_4 = Zan::Mar.create!(:koo => @koo_3)
|
|
582
582
|
end
|
|
583
583
|
|
|
584
584
|
after do
|
|
585
|
-
ActiveRecord::Migration.drop_table(:
|
|
585
|
+
ActiveRecord::Migration.drop_table(:zan_zabs)
|
|
586
586
|
ActiveRecord::Migration.drop_table(:zan_mars)
|
|
587
587
|
ActiveRecord::Migration.drop_table(:zan_koos)
|
|
588
588
|
end
|
|
589
589
|
|
|
590
|
-
it "should find the two records that are related to a
|
|
591
|
-
Zan::Koo.search_for('
|
|
590
|
+
it "should find the two records that are related to a zab record" do
|
|
591
|
+
Zan::Koo.search_for('zab').length.should == 2
|
|
592
592
|
end
|
|
593
593
|
|
|
594
|
-
it "should find the one record that is related to two
|
|
595
|
-
Zan::Koo.search_for('related=
|
|
594
|
+
it "should find the one record that is related to two zab records" do
|
|
595
|
+
Zan::Koo.search_for('related=zab AND related="zab too!"').length.should == 1
|
|
596
596
|
end
|
|
597
597
|
end
|
|
598
598
|
|
|
@@ -43,8 +43,9 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
|
43
43
|
@class.search_for("uuid != #{@record3.uuid}").length.should == 2
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
it "should find a record by just specifying the uuid" do
|
|
46
|
+
it "should find a record by just specifying the uuid (case insensitive)" do
|
|
47
47
|
@class.search_for(@record1.uuid).first.uuid.should == @record1.uuid
|
|
48
|
+
@class.search_for(@record1.uuid.upcase).first.uuid.should == @record1.uuid
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
it "should not find a record if the uuid is not a valid uuid" do
|
data/spec/unit/parser_spec.rb
CHANGED
|
@@ -102,7 +102,27 @@ describe ScopedSearch::QueryLanguage::Parser do
|
|
|
102
102
|
'set? a b null? c'.should parse_to([:and, [:notnull, 'a'], 'b', [:null, 'c']])
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
it 'should parse logical operators with a single argument' do
|
|
106
|
+
'& a'.should parse_to('a')
|
|
107
|
+
'& & & & a & b & & &'.should parse_to([:and, 'a', 'b'])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'should parse in and not in operators with no lhs' do
|
|
111
|
+
'^ a'.should parse_to([:eq, 'a'])
|
|
112
|
+
'^ a b'.should parse_to([:or, [:eq, 'a'], [:eq, 'b']])
|
|
113
|
+
'^ a,b'.should parse_to([:or, [:eq, 'a'], [:eq, 'b']])
|
|
114
|
+
|
|
115
|
+
'^ (a b)'.should parse_to([:or, [:eq, 'a'], [:eq, 'b']])
|
|
116
|
+
'^ (a,b)'.should parse_to([:or, [:eq, 'a'], [:eq, 'b']])
|
|
117
|
+
|
|
118
|
+
'!^ a'.should parse_to([:ne, 'a'])
|
|
119
|
+
'!^ a b'.should parse_to([:and, [:ne, 'a'], [:ne, 'b']])
|
|
120
|
+
'!^ a,b'.should parse_to([:and, [:ne, 'a'], [:ne, 'b']])
|
|
121
|
+
'!^ (a b)'.should parse_to([:and, [:ne, 'a'], [:ne, 'b']])
|
|
122
|
+
'!^ (a,b)'.should parse_to([:and, [:ne, 'a'], [:ne, 'b']])
|
|
123
|
+
end
|
|
124
|
+
|
|
105
125
|
it "should refuse to parse an empty not expression" do
|
|
106
126
|
lambda { ScopedSearch::QueryLanguage::Compiler.parse('!()|*') }.should raise_error(ScopedSearch::QueryNotSupported)
|
|
107
|
-
end
|
|
127
|
+
end
|
|
108
128
|
end
|
|
@@ -155,4 +155,65 @@ describe ScopedSearch::QueryBuilder do
|
|
|
155
155
|
lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported, /failed with error: test/)
|
|
156
156
|
end
|
|
157
157
|
end
|
|
158
|
+
|
|
159
|
+
context 'datetime_test' do
|
|
160
|
+
before(:each) do
|
|
161
|
+
@field = double('field')
|
|
162
|
+
@query_builder = ScopedSearch::QueryBuilder.new(@definition, nil, nil)
|
|
163
|
+
|
|
164
|
+
@field.stub(:datetime?).and_return(true)
|
|
165
|
+
@field.stub(:date?).and_return(false)
|
|
166
|
+
@field.stub(:to_sql).and_return('started_at')
|
|
167
|
+
|
|
168
|
+
[:virtual?, :set?, :temporal?, :relation, :offset].each { |key| @field.stub(key).and_return(false) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "should return correct SQL literal for equality operator" do
|
|
172
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2023, 10, 10))
|
|
173
|
+
result = @query_builder.datetime_test(@field, :eq, '2023-10-10') { |type, value| }
|
|
174
|
+
result.should eq(["(started_at >= ? AND started_at < ?)", DateTime.new(2023, 10, 10), DateTime.new(2023, 10, 11)])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "should return correct SQL literal for inequality operator" do
|
|
178
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2023, 10, 10))
|
|
179
|
+
result = @query_builder.datetime_test(@field, :ne, '2023-10-10') { |type, value| }
|
|
180
|
+
result.should eq(["NOT (started_at >= ? AND started_at < ?)", DateTime.new(2023, 10, 10), DateTime.new(2023, 10, 11)])
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "should return correct SQL literal for greater operator" do
|
|
184
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2023, 10, 9))
|
|
185
|
+
result = @query_builder.datetime_test(@field, :gt, '2023-10-9') { |type, value| }
|
|
186
|
+
result.should eq(["started_at >= ?", DateTime.new(2023, 10, 10)])
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "should return correct SQL literal for less than or equal operator" do
|
|
190
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2023, 10, 10))
|
|
191
|
+
result = @query_builder.datetime_test(@field, :lte, '2023-10-10') { |type, value| }
|
|
192
|
+
result.should eq(["started_at < ?", DateTime.new(2023, 10, 11)])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "should return empty array for invalid date" do
|
|
196
|
+
@definition.stub(:parse_temporal).and_return(nil)
|
|
197
|
+
result = @query_builder.datetime_test(@field, :eq, 'invalid-date') { |type, value| }
|
|
198
|
+
result.should eq([])
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "should count with 1 month deviation if only year and month is provided" do
|
|
202
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2024, 1, 1))
|
|
203
|
+
result = @query_builder.datetime_test(@field, :gt, 'January 2024') { |type, value| }
|
|
204
|
+
result.should eq(["started_at >= ?", DateTime.new(2024, 2, 1)])
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "should not count with deviation if minute is the smallest unit provided" do
|
|
208
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2023, 10, 10, 13, 0, 0))
|
|
209
|
+
result = @query_builder.datetime_test(@field, :gt, '2023-10-10 13:00') { |type, value| }
|
|
210
|
+
result.should eq(["started_at > ?", DateTime.new(2023, 10, 10, 13, 0, 0)])
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "should not count with deviation if second is the smallest unit provided" do
|
|
214
|
+
@definition.stub(:parse_temporal).and_return(DateTime.new(2023, 10, 10, 13, 0, 0, 1))
|
|
215
|
+
result = @query_builder.datetime_test(@field, :gt, '2023-10-10 13:00:01') { |type, value| }
|
|
216
|
+
result.should eq(["started_at > ?", DateTime.new(2023, 10, 10, 13, 0, 0, 1)])
|
|
217
|
+
end
|
|
218
|
+
end
|
|
158
219
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scoped_search
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.1
|
|
4
|
+
version: 4.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amos Benari
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date:
|
|
13
|
+
date: 2025-09-30 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: activerecord
|
|
@@ -54,6 +54,76 @@ dependencies:
|
|
|
54
54
|
- - ">="
|
|
55
55
|
- !ruby/object:Gem::Version
|
|
56
56
|
version: '0'
|
|
57
|
+
- !ruby/object:Gem::Dependency
|
|
58
|
+
name: base64
|
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
- !ruby/object:Gem::Dependency
|
|
72
|
+
name: benchmark
|
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
type: :development
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
- !ruby/object:Gem::Dependency
|
|
86
|
+
name: bigdecimal
|
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
type: :development
|
|
93
|
+
prerelease: false
|
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
- !ruby/object:Gem::Dependency
|
|
100
|
+
name: logger
|
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
type: :development
|
|
107
|
+
prerelease: false
|
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '0'
|
|
113
|
+
- !ruby/object:Gem::Dependency
|
|
114
|
+
name: mutex_m
|
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
type: :development
|
|
121
|
+
prerelease: false
|
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '0'
|
|
57
127
|
description: |2
|
|
58
128
|
Scoped search makes it easy to search your ActiveRecord-based models.
|
|
59
129
|
|
|
@@ -165,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
165
235
|
- !ruby/object:Gem::Version
|
|
166
236
|
version: '0'
|
|
167
237
|
requirements: []
|
|
168
|
-
rubygems_version: 3.1
|
|
238
|
+
rubygems_version: 3.0.3.1
|
|
169
239
|
signing_key:
|
|
170
240
|
specification_version: 4
|
|
171
241
|
summary: Easily search you ActiveRecord models with a simple query language using
|