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 +4 -4
- data/lib/scoped_search/query_builder.rb +24 -4
- data/lib/scoped_search/query_language/parser.rb +29 -7
- data/lib/scoped_search/version.rb +1 -1
- data/lib/scoped_search.rb +1 -0
- data/scoped_search.gemspec +7 -0
- data/spec/unit/parser_spec.rb +21 -1
- data/spec/unit/query_builder_spec.rb +61 -0
- metadata +76 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 958718e625d5bbb80f93ea868abebac96d29a54e712c6c0275f9e6a1e44e07d3
|
4
|
+
data.tar.gz: 90f57db3c2009746a0beb9d843bee7a46d55d1b0ef02d796c12490d7b3e7f1b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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}"
|
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
|
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,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scoped_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
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:
|
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.
|
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
|