scoped_search 4.1.12 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|