scoped_search 2.3.6 → 2.3.7
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/scoped_search/definition.rb +9 -9
- data/lib/scoped_search/query_builder.rb +16 -4
- data/lib/scoped_search/query_language/parser.rb +18 -4
- data/lib/scoped_search/query_language/tokenizer.rb +2 -2
- data/lib/scoped_search.rb +1 -1
- data/scoped_search.gemspec +3 -2
- data/spec/integration/auto_complete_spec.rb +2 -2
- metadata +24 -10
@@ -149,9 +149,9 @@ module ScopedSearch
|
|
149
149
|
register_complete_for! unless klass.respond_to?(:complete_for)
|
150
150
|
|
151
151
|
end
|
152
|
-
|
152
|
+
|
153
153
|
attr_accessor :profile, :default_order
|
154
|
-
|
154
|
+
|
155
155
|
def fields
|
156
156
|
@profile ||= :default
|
157
157
|
@profile_fields[@profile] ||= {}
|
@@ -176,11 +176,11 @@ module ScopedSearch
|
|
176
176
|
def operator_by_field_name(name)
|
177
177
|
field = field_by_name(name)
|
178
178
|
return [] if field.nil?
|
179
|
-
return field.operators
|
180
|
-
return ['= ', '!= ']
|
181
|
-
return ['= ', '> ', '< ', '<= ', '>= ','!= '] if field.numerical?
|
182
|
-
return ['= ', '!= ', '~ ', '!~ '] if field.textual?
|
183
|
-
return ['= ', '> ', '< ']
|
179
|
+
return field.operators if field.operators
|
180
|
+
return ['= ', '!= '] if field.set?
|
181
|
+
return ['= ', '> ', '< ', '<= ', '>= ','!= ', '^ ', '!^ '] if field.numerical?
|
182
|
+
return ['= ', '!= ', '~ ', '!~ ', '^ ', '!^ '] if field.textual?
|
183
|
+
return ['= ', '> ', '< '] if field.temporal?
|
184
184
|
raise ScopedSearch::QueryNotSupported, "could not verify '#{name}' type, this can be a result of a definition error"
|
185
185
|
end
|
186
186
|
|
@@ -230,8 +230,8 @@ module ScopedSearch
|
|
230
230
|
when 2
|
231
231
|
@klass.named_scope(:search_for, lambda { |*args| ScopedSearch::QueryBuilder.build_query(self, args[0], args[1]) })
|
232
232
|
when 3
|
233
|
-
@klass.scope(:search_for, lambda { |*args|
|
234
|
-
find_options = ScopedSearch::QueryBuilder.build_query(self, args[0], args[1])
|
233
|
+
@klass.scope(:search_for, lambda { |*args|
|
234
|
+
find_options = ScopedSearch::QueryBuilder.build_query(self, args[0], args[1])
|
235
235
|
search_scope = @klass.scoped
|
236
236
|
search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
|
237
237
|
search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
|
@@ -101,7 +101,8 @@ module ScopedSearch
|
|
101
101
|
|
102
102
|
# A hash that maps the operators of the query language with the corresponding SQL operator.
|
103
103
|
SQL_OPERATORS = { :eq =>'=', :ne => '<>', :like => 'LIKE', :unlike => 'NOT LIKE',
|
104
|
-
:gt => '>', :lt =>'<', :lte => '<=', :gte => '>='
|
104
|
+
:gt => '>', :lt =>'<', :lte => '<=', :gte => '>=',
|
105
|
+
:in => 'IN',:notin => 'NOT IN' }
|
105
106
|
|
106
107
|
# Return the SQL operator to use given an operator symbol and field definition.
|
107
108
|
#
|
@@ -170,9 +171,16 @@ module ScopedSearch
|
|
170
171
|
end
|
171
172
|
|
172
173
|
# Validate the key name is in the set and translate the value to the set value.
|
174
|
+
def translate_value(field, value)
|
175
|
+
translated_value = field.complete_value[value.to_sym]
|
176
|
+
raise ScopedSearch::QueryNotSupported, "'#{field.field}' should be one of '#{field.complete_value.keys.join(', ')}', but the query was '#{value}'" if translated_value.nil?
|
177
|
+
translated_value
|
178
|
+
end
|
179
|
+
|
180
|
+
# A 'set' is group of possible values, for example a status might be "on", "off" or "unknown" and the database representation
|
181
|
+
# could be for example a numeric value. This method will validate the input and translate it into the database representation.
|
173
182
|
def set_test(field, operator,value, &block)
|
174
|
-
set_value = field
|
175
|
-
raise ScopedSearch::QueryNotSupported, "'#{field.field}' should be one of '#{field.complete_value.keys.join(', ')}', but the query was '#{value}'" if set_value.nil?
|
183
|
+
set_value = translate_value(field, value)
|
176
184
|
raise ScopedSearch::QueryNotSupported, "Operator '#{operator}' not supported for '#{field.field}'" unless [:eq,:ne].include?(operator)
|
177
185
|
negate = ''
|
178
186
|
if [true,false].include?(set_value)
|
@@ -205,6 +213,10 @@ module ScopedSearch
|
|
205
213
|
if [:like, :unlike].include?(operator)
|
206
214
|
yield(:parameter, (value !~ /^\%|\*/ && value !~ /\%|\*$/) ? "%#{value}%" : value.tr_s('%*', '%'))
|
207
215
|
return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
|
216
|
+
elsif [:in, :notin].include?(operator)
|
217
|
+
value.split(',').collect { |v| yield(:parameter, field.set? ? translate_value(field, v) : v.strip) }
|
218
|
+
value = value.split(',').collect { "?" }.join(",")
|
219
|
+
return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} (#{value})"
|
208
220
|
elsif field.temporal?
|
209
221
|
return datetime_test(field, operator, value, &block)
|
210
222
|
elsif field.set?
|
@@ -408,7 +420,7 @@ module ScopedSearch
|
|
408
420
|
# Defines the to_sql method for AST AND/OR operators
|
409
421
|
module LogicalOperatorNode
|
410
422
|
def to_sql(builder, definition, &block)
|
411
|
-
fragments = children.map { |c| c.to_sql(builder, definition, &block) }.
|
423
|
+
fragments = children.map { |c| c.to_sql(builder, definition, &block) }.map { |sql| "(#{sql})" unless sql.blank? }.compact
|
412
424
|
fragments.empty? ? nil : "#{fragments.join(" #{operator.to_s.upcase} ")}"
|
413
425
|
end
|
414
426
|
end
|
@@ -11,7 +11,7 @@ module ScopedSearch::QueryLanguage::Parser
|
|
11
11
|
LOGICAL_INFIX_OPERATORS = [:and, :or]
|
12
12
|
LOGICAL_PREFIX_OPERATORS = [:not]
|
13
13
|
NULL_PREFIX_OPERATORS = [:null, :notnull]
|
14
|
-
COMPARISON_OPERATORS = [:eq, :ne, :gt, :gte, :lt, :lte, :like, :unlike]
|
14
|
+
COMPARISON_OPERATORS = [:eq, :ne, :gt, :gte, :lt, :lte, :like, :unlike, :in, :notin]
|
15
15
|
ALL_INFIX_OPERATORS = LOGICAL_INFIX_OPERATORS + COMPARISON_OPERATORS
|
16
16
|
ALL_PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + COMPARISON_OPERATORS + NULL_PREFIX_OPERATORS
|
17
17
|
|
@@ -99,11 +99,25 @@ module ScopedSearch::QueryLanguage::Parser
|
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
|
-
#
|
102
|
+
# Parse values in the format (val, val, val)
|
103
|
+
def parse_multiple_values
|
104
|
+
next_token if peek_token == :lparen #skip :lparen
|
105
|
+
value = []
|
106
|
+
value << current_token if String === next_token until peek_token.nil? || peek_token == :rparen
|
107
|
+
next_token if peek_token == :rparen # consume the :rparen
|
108
|
+
value.join(',')
|
109
|
+
end
|
110
|
+
|
103
111
|
# This can either be a constant value or a field name.
|
104
112
|
def parse_value
|
105
|
-
|
106
|
-
|
113
|
+
if String === peek_token
|
114
|
+
ScopedSearch::QueryLanguage::AST::LeafNode.new(next_token)
|
115
|
+
elsif ([:in, :notin].include? current_token)
|
116
|
+
value = parse_multiple_values()
|
117
|
+
ScopedSearch::QueryLanguage::AST::LeafNode.new(value)
|
118
|
+
else
|
119
|
+
raise ScopedSearch::QueryNotSupported, "Value expected but found #{peek_token.inspect}"
|
120
|
+
end
|
107
121
|
end
|
108
122
|
|
109
123
|
protected
|
@@ -7,7 +7,7 @@ module ScopedSearch::QueryLanguage::Tokenizer
|
|
7
7
|
|
8
8
|
# Every operator the language supports.
|
9
9
|
OPERATORS = { '&' => :and, '|' => :or, '&&' => :and, '||' => :or, '-'=> :not, '!' => :not, '~' => :like, '!~' => :unlike,
|
10
|
-
'=' => :eq, '==' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
|
10
|
+
'=' => :eq, '==' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte, '^' => :in, '!^' => :notin }
|
11
11
|
|
12
12
|
# Tokenizes the string and returns the result as an array of tokens.
|
13
13
|
def tokenize
|
@@ -41,7 +41,7 @@ module ScopedSearch::QueryLanguage::Tokenizer
|
|
41
41
|
when '('; yield(:lparen)
|
42
42
|
when ')'; yield(:rparen)
|
43
43
|
when ','; yield(:comma)
|
44
|
-
when
|
44
|
+
when /\&|\||=|<|>|\^|!|~|-/; tokenize_operator(&block)
|
45
45
|
when '"'; tokenize_quoted_keyword(&block)
|
46
46
|
else; tokenize_keyword(&block)
|
47
47
|
end
|
data/lib/scoped_search.rb
CHANGED
@@ -14,7 +14,7 @@ module ScopedSearch
|
|
14
14
|
|
15
15
|
# The current scoped_search version. Do not change thisvalue by hand,
|
16
16
|
# because it will be updated automatically by the gem release script.
|
17
|
-
VERSION = "2.3.
|
17
|
+
VERSION = "2.3.7"
|
18
18
|
|
19
19
|
# The ClassMethods module will be included into the ActiveRecord::Base class
|
20
20
|
# to add the <tt>ActiveRecord::Base.scoped_search</tt> method and the
|
data/scoped_search.gemspec
CHANGED
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
|
|
3
3
|
|
4
4
|
# Do not change the version and date fields by hand. This will be done
|
5
5
|
# automatically by the gem release script.
|
6
|
-
s.version = "2.3.
|
7
|
-
s.date = "
|
6
|
+
s.version = "2.3.7"
|
7
|
+
s.date = "2012-04-30"
|
8
8
|
|
9
9
|
s.summary = "Easily search you ActiveRecord models with a simple query language using a named scope."
|
10
10
|
s.description = <<-EOS
|
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
|
|
26
26
|
|
27
27
|
s.add_runtime_dependency('activerecord', '>= 2.1.0')
|
28
28
|
s.add_development_dependency('rspec', '~> 2.0')
|
29
|
+
s.add_development_dependency('rake')
|
29
30
|
|
30
31
|
s.add_development_dependency('sqlite3-ruby')
|
31
32
|
|
@@ -47,11 +47,11 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
47
47
|
end
|
48
48
|
|
49
49
|
it "should complete the string comparators" do
|
50
|
-
Foo.complete_for('string ').should =~ (["string != ", "string !~ ", "string = ", "string ~ "])
|
50
|
+
Foo.complete_for('string ').should =~ (["string != ", "string !^ ", "string !~ ", "string = ", "string ^ ", "string ~ "])
|
51
51
|
end
|
52
52
|
|
53
53
|
it "should complete the numerical comparators" do
|
54
|
-
Foo.complete_for('int ').should =~ (["int != ", "int < ", "int <= ", "int = ", "int > ", "int >= "])
|
54
|
+
Foo.complete_for('int ').should =~ (["int != ", "int !^ ", "int < ", "int <= ", "int = ", "int > ", "int >= ", "int ^ "])
|
55
55
|
end
|
56
56
|
|
57
57
|
it "should complete the temporal (date) comparators" do
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scoped_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
version: 2.3.
|
9
|
+
- 7
|
10
|
+
version: 2.3.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Willem van Bergen
|
@@ -16,12 +16,10 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date:
|
19
|
+
date: 2012-04-30 00:00:00 +03:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
23
|
-
name: activerecord
|
24
|
-
prerelease: false
|
25
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
26
24
|
none: false
|
27
25
|
requirements:
|
@@ -34,10 +32,10 @@ dependencies:
|
|
34
32
|
- 0
|
35
33
|
version: 2.1.0
|
36
34
|
type: :runtime
|
35
|
+
name: activerecord
|
36
|
+
prerelease: false
|
37
37
|
version_requirements: *id001
|
38
38
|
- !ruby/object:Gem::Dependency
|
39
|
-
name: rspec
|
40
|
-
prerelease: false
|
41
39
|
requirement: &id002 !ruby/object:Gem::Requirement
|
42
40
|
none: false
|
43
41
|
requirements:
|
@@ -49,10 +47,10 @@ dependencies:
|
|
49
47
|
- 0
|
50
48
|
version: "2.0"
|
51
49
|
type: :development
|
50
|
+
name: rspec
|
51
|
+
prerelease: false
|
52
52
|
version_requirements: *id002
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
|
-
name: sqlite3-ruby
|
55
|
-
prerelease: false
|
56
54
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
55
|
none: false
|
58
56
|
requirements:
|
@@ -63,7 +61,23 @@ dependencies:
|
|
63
61
|
- 0
|
64
62
|
version: "0"
|
65
63
|
type: :development
|
64
|
+
name: rake
|
65
|
+
prerelease: false
|
66
66
|
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
type: :development
|
78
|
+
name: sqlite3-ruby
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *id004
|
67
81
|
description: " Scoped search makes it easy to search your ActiveRecord-based models.\n \n It will create a named scope :search_for that can be called with a query string. It will build an SQL query using\n the provided query string and a definition that specifies on what fields to search. Because the functionality is\n built on named_scope, the result of the search_for call can be used like any other named_scope, so it can be\n chained with another scope or combined with will_paginate.\n \n Because it uses standard SQL, it does not require any setup, indexers or daemons. This makes scoped_search\n suitable to quickly add basic search functionality to your application with little hassle. On the other hand,\n it may not be the best choice if it is going to be used on very large datasets or by a large user base.\n"
|
68
82
|
email:
|
69
83
|
- willem@railsdoctors.com
|