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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fe5811549cf2e222ac16ea429293269abdd0734793b9e83611e76527acd905b
4
- data.tar.gz: 22463f4c28a03919dc7e42cd4942dcc3d66c0291bcb087fa5ad1688492726efd
3
+ metadata.gz: 2413a0e2075ccff25188af0a579df4b8cfde593f38ab801b99bd7379b2344c8b
4
+ data.tar.gz: edc1ec5d32cc2b410fdf9938e49aca0cfba46807850491c3ee6bba28642dfc5b
5
5
  SHA512:
6
- metadata.gz: 3e36707917762b0f2759da997bceb96b2f91b91bcbdb920d03a1738e408bc72a545e380f55bd5c35b9448d2916ac7ddb5dcc0ec623b6a36726d464ac818e15cb
7
- data.tar.gz: 1b0e4c026cffd08a3851a5929dd8ee7244c041c27d282789bac19fb7af77e2b0641e8d80cf52fcf8f8318190f15c647bcdf76ac98b3576c537a65a5250ec7784
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.to_s =~ /\s/ ? "\"#{v.gsub('"', '\"')}\"" : v }
219
+ .map { |v| v.is_a?(String) ? "\"#{v.gsub('"', '\"')}\"" : v }
219
220
  end
220
221
 
221
222
  def completer_scope(field)
222
- klass = field.klass
223
- scope = klass.respond_to?(:completer_scope) ? klass.completer_scope(@options) : klass
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
- span = 1.minute if(value =~ /\A\s*\d+\s+\bminutes?\b\s+\bago\b\s*\z/i)
150
- span ||= (timestamp.day_fraction == 0) ? 1.day : 1.hour
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
- operator = next_token
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
- return ScopedSearch::QueryLanguage::AST::OperatorNode.new(next_token, [parse_value])
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.join(',')
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
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "4.1.12"
2
+ VERSION = "4.3.1"
3
3
  end
data/lib/scoped_search.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'logger' # a workaround for https://github.com/rails/rails/issues/54263
1
2
  require 'active_record'
2
3
 
3
4
  # ScopedSearch is the base module for the scoped_search plugin. This file
@@ -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 required" do
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 :baz_id }
253
- ActiveRecord::Migration.create_table(:bazs) { |t| t.string :related }
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 :baz; belongs_to :koo; end
258
- class Baz < ActiveRecord::Base; has_many :mars; end
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 :bazs, :through => :mars, :source => :baz
266
+ has_many :zabs, :through => :mars, :source => :zab
267
267
 
268
- scoped_search :relation => :bazs, :on => :related
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
- @baz_1 = Baz.create(:related => 'baz')
276
- @baz_2 = Baz.create(:related => 'baz too!')
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, :baz => @baz_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, :baz => @baz_1)
281
- @bar_3 = Mar.create!(:koo => @koo_2, :baz => @baz_2)
282
- @bar_3 = Mar.create!(:koo => @koo_2, :baz => @baz_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(:bazs)
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 baz record" do
293
- Koo.search_for('baz').length.should == 2
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 baz record" do
297
- Koo.search_for('related=baz AND related="baz too!"').length.should == 1
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 :baz_id }
307
- ActiveRecord::Migration.create_table(:bazs) { |t| t.string :related }
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 :baz; has_many :zoos; end
312
- class Baz < ActiveRecord::Base; has_many :zars; end
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 :bazs, :through => :zar
317
+ has_many :zabs, :through => :zar
318
318
 
319
- scoped_search :relation => :bazs, :on => :related
319
+ scoped_search :relation => :zabs, :on => :related
320
320
  end
321
321
 
322
- baz_1 = Baz.create(:related => 'baz')
323
- baz_2 = Baz.create(:related => 'baz too!')
322
+ zab_1 = Zab.create(:related => 'zab')
323
+ zab_2 = Zab.create(:related => 'zab too!')
324
324
 
325
- zar_1 = Zar.create!( :baz => baz_1)
326
- zar_2 = Zar.create!( :baz => baz_2)
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(:bazs)
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 baz record" do
340
- Zoo.search_for('baz').length.should == 3
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 baz record" do
344
- Zoo.search_for('related=baz AND related="baz too!"').length.should == 0
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 => 'baz')
396
- @dog_2 = Dog.create(:related => 'baz too!')
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=baz').length.should == 2
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('baz').length.should == 3
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 :baz_id }
551
- ActiveRecord::Migration.create_table(:zan_bazs) { |t| t.string :related }
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 :baz; belongs_to :koo; self.table_name = "zan_mars"; end; end
556
- module Zan; class Baz < ActiveRecord::Base; has_many :mars; self.table_name = "zan_bazs"; end; end
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 :bazs, :through => :mars
562
+ has_many :zabs, :through => :mars
563
563
  self.table_name = "zan_koos"
564
564
 
565
- scoped_search :relation => :bazs, :on => :related
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
- @baz_1 = Zan::Baz.create(:related => 'baz')
574
- @baz_2 = Zan::Baz.create(:related => 'baz too!')
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, :baz => @baz_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, :baz => @baz_1)
579
- @bar_3 = Zan::Mar.create!(:koo => @koo_2, :baz => @baz_2)
580
- @bar_3 = Zan::Mar.create!(:koo => @koo_2, :baz => @baz_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(:zan_bazs)
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 baz record" do
591
- Zan::Koo.search_for('baz').length.should == 2
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 baz records" do
595
- Zan::Koo.search_for('related=baz AND related="baz too!"').length.should == 1
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
@@ -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.12
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: 2023-10-26 00:00:00.000000000 Z
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.6
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