scoped_search 4.1.12 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fe5811549cf2e222ac16ea429293269abdd0734793b9e83611e76527acd905b
4
- data.tar.gz: 22463f4c28a03919dc7e42cd4942dcc3d66c0291bcb087fa5ad1688492726efd
3
+ metadata.gz: 958718e625d5bbb80f93ea868abebac96d29a54e712c6c0275f9e6a1e44e07d3
4
+ data.tar.gz: 90f57db3c2009746a0beb9d843bee7a46d55d1b0ef02d796c12490d7b3e7f1b8
5
5
  SHA512:
6
- metadata.gz: 3e36707917762b0f2759da997bceb96b2f91b91bcbdb920d03a1738e408bc72a545e380f55bd5c35b9448d2916ac7ddb5dcc0ec623b6a36726d464ac818e15cb
7
- data.tar.gz: 1b0e4c026cffd08a3851a5929dd8ee7244c041c27d282789bac19fb7af77e2b0641e8d80cf52fcf8f8318190f15c647bcdf76ac98b3576c537a65a5250ec7784
6
+ metadata.gz: 2cc38b0ce04452df7b7205465621c0624d1173c9e9a95eb849f50eaa99fdcad9545a7411aec5fae2a3c787f348a3f440244c54e8623a3ed2589d47735110ef8b
7
+ data.tar.gz: 60ebb019e199a50505e0e934a1e2fd405a03c903350554c8f13ca62f85c5acc472b073a6bf1ebcdb1f44b15c54ec593dc31b9bf474782ff739803a0d94f43e81
@@ -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}"
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "4.1.12"
2
+ VERSION = "4.2.0"
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
@@ -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,16 +1,16 @@
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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amos Benari
8
8
  - Willem van Bergen
9
9
  - Wes Hays
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-10-26 00:00:00.000000000 Z
13
+ date: 2025-02-18 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
 
@@ -144,7 +214,7 @@ homepage: https://github.com/wvanbergen/scoped_search/wiki
144
214
  licenses:
145
215
  - MIT
146
216
  metadata: {}
147
- post_install_message:
217
+ post_install_message:
148
218
  rdoc_options:
149
219
  - "--title"
150
220
  - scoped_search
@@ -165,8 +235,8 @@ 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
169
- signing_key:
238
+ rubygems_version: 3.3.27
239
+ signing_key:
170
240
  specification_version: 4
171
241
  summary: Easily search you ActiveRecord models with a simple query language using
172
242
  a named scope