scoped_search 2.0.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.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ scoped_search-*.gem
3
+ /tmp
4
+ /doc
5
+ /pkg
6
+ /coverage
7
+ /classes
8
+ /files
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Willem van Bergen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,107 @@
1
+ = Scoped search
2
+
3
+ The <b>scoped_search</b> Rails plugin makes it easy to search your ActiveRecord
4
+ models. Searching is performed using a query string, which should be passed to
5
+ the named_scope <tt>search_for</tt>. Based on a definition in what fields to
6
+ look, it will build query conditions and return those as a named scope.
7
+
8
+ Scoped search is great if you want to offer a simple search box to your users
9
+ and build a query based on the search string they enter. If you want to build a
10
+ more complex search form with multiple fields, searchlogic (see
11
+ http://github.com/binarylogic/searchlogic) may be a good choice for you.
12
+
13
+
14
+ == Installing
15
+
16
+ The recommended method to enable scoped_search in your project is adding the scoped_search gem to your environment. Add the following code to your Rails configuration in <tt>config/environment.rb</tt>:
17
+
18
+ Rails::Initializer.run do |config|
19
+ ...
20
+ config.gem 'wvanbergen-scoped_search', :lib => 'scoped_search',
21
+ :source => 'http://gems.github.com/'
22
+ end
23
+
24
+ Run <tt>sudo rake gems:install</tt> to install the gem.
25
+
26
+ Alternatively, install scoped_search as a Rails plugin:
27
+
28
+ script/plugin install git://github.com/wvanbergen/scoped_search.git
29
+
30
+ == Usage
31
+
32
+ Scoped search requires you to define the fields you want to search in:
33
+
34
+ class User < ActiveRecord::Base
35
+ scoped_search :on => [:first_name, :last_name]
36
+ end
37
+
38
+ For more information about options and using fields from relations, see the
39
+ project wiki on search definitions:
40
+ http://wiki.github.com/wvanbergen/scoped_search/search-definition
41
+
42
+ Now, the <b>search_for</b> scope is available for queries. You should pass a
43
+ query string to the scope. This can be empty or nil, in which case all no search
44
+ conditions are set (and all records will be returned).
45
+
46
+ User.search_for('my search string').each { |user| ... }
47
+
48
+ The result is returned as <tt>named_scope</tt>. Because of this, you can
49
+ actually chain the call with other scopes, or with will_paginate. An example:
50
+
51
+ class Project < ActiveRecord::Base
52
+ searchable_on :name, :description
53
+ named_scope :public, :conditions => {:public => true }
54
+ end
55
+
56
+ # using chained named_scopes and will_paginate in your controller
57
+ Project.public.search_for(params[:q]).paginate(:page => params[:page], :include => :tasks)
58
+
59
+ More information about usage can be found in the project wiki:
60
+ http://wiki.github.com/wvanbergen/scoped_search/usage
61
+
62
+ == Query language
63
+
64
+ The search query language is simple, but supports several constructs to support
65
+ more complex queries:
66
+
67
+ words:: require every word to be present, e.g.: <tt>some search keywords</tt>
68
+
69
+ phrases:: use quotes for multi-word phrases, e.g. <tt>"police car"</tt>
70
+
71
+ negation:: look for "everything but", e.g. <tt>police -uniform</tt>, <tt>-"police car"</tt>,
72
+ <tt>police NOT car</tt>
73
+
74
+ logical keywords:: make logical constructs using AND, OR, &&, ||, &, | operators, e.g.
75
+ <tt>uniform OR car</tt>, <tt>scoped && search</tt>
76
+
77
+ parentheses:: to structure logic e.g. <tt>"police AND (uniform OR car)"</tt>
78
+
79
+ comparison operators:: to search in numerical or temporal fields, e.g.
80
+ <tt>> 22</tt>, <tt>< 2009-01-01</tt>
81
+
82
+ explicit fields:: search only in the given field. e.g. <tt>username = root</tt>,
83
+ <tt>created_at > 2009-01-01</tt>
84
+
85
+ NULL checks:: using the <tt>set?</tt> and <tt>null?</tt> operator with a field name, e.g.
86
+ <tt>null? graduated_at</tt>, <tt>set? parent_id</tt>
87
+
88
+ A complex query example to look for Ruby on Rails programmers without cobol
89
+ experience, over 18 years old, with a recently updated record and a non-lame
90
+ nickname:
91
+
92
+ ("Ruby" OR "Rails") -cobol, age >= 18, updated_at > 2009-01-01 && nickname !~ l33t
93
+
94
+ For more info, see the the project wiki: http://wiki.github.com/wvanbergen/scoped_search/query-language
95
+
96
+ == Additional resources
97
+
98
+ * Source code: http://github.com/wvanbergen/scoped_search/tree/master
99
+ * Project wiki: http://wiki.github.com/wvanbergen/scoped_search
100
+ * RDoc documentation: http://wvanbergen.github.com/scoped_search
101
+ * wvanbergen's blog posts: http://techblog.floorplanner.com/tag/scoped_search
102
+
103
+ == License
104
+
105
+ This plugin is released under the MIT license. Please contact weshays
106
+ (http://github.com/weshays) or wvanbergen (http://github.com/wvanbergen) for any
107
+ questions.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ Dir['tasks/*.rake'].each { |file| load(file) }
2
+
3
+ GithubGem::RakeTasks.new(:gem)
4
+ task :default => [:spec]
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'scoped_search'
@@ -0,0 +1,91 @@
1
+ # ScopedSearch is the base module for the scoped_search plugin. This file
2
+ # defines some modules and exception classes, loads the necessary files, and
3
+ # installs itself in ActiveRecord.
4
+ #
5
+ # The ScopedSearch module defines two modules that can be mixed into
6
+ # ActiveRecord::Base as class methods. <tt>ScopedSearch::ClassMethods</tt>
7
+ # will register the scoped_search class function, which can be used to define
8
+ # the search fields. <tt>ScopedSearch::BackwardsCompatibility</tt> will
9
+ # register the <tt>searchable_on</tt> method for backwards compatibility with
10
+ # previous scoped_search versions (1.x).
11
+ module ScopedSearch
12
+
13
+ # The current scoped_search version. Do not change thisvalue by hand,
14
+ # because it will be updated automatically by the gem release script.
15
+ VERSION = "2.0.1"
16
+
17
+ # The ClassMethods module will be included into the ActiveRecord::Base class
18
+ # to add the <tt>ActiveRecord::Base.scoped_search</tt> method and the
19
+ # <tt>ActiveRecord::Base.search_for</tt> named scope.
20
+ module ClassMethods
21
+
22
+ # Export the scoped_search method fo defining the search options.
23
+ # This method will create a definition instance for the class if it does not yet exist,
24
+ # and use the object as block argument and retun value.
25
+ def scoped_search(*definitions)
26
+ @scoped_search ||= ScopedSearch::Definition.new(self)
27
+ definitions.each do |definition|
28
+ if definition[:on].kind_of?(Array)
29
+ definition[:on].each { |field| @scoped_search.define(definition.merge(:on => field)) }
30
+ else
31
+ @scoped_search.define(definition)
32
+ end
33
+ end
34
+ return @scoped_search
35
+ end
36
+ end
37
+
38
+ # The <tt>BackwardsCompatibility</tt> module can be included into
39
+ # <tt>ActiveRecord::Base</tt> to provide the <tt>searchable_on</tt> search
40
+ # field definition syntax that is compatible with scoped_seach 1.x
41
+ #
42
+ # Currently, it is included into <tt>ActiveRecord::Base</tt> by default, but
43
+ # this may change in the future. So, please uodate to the newer syntax as
44
+ # soon as possible.
45
+ module BackwardsCompatibility
46
+
47
+ # Defines fields to search on using a syntax compatible with scoped_search 1.x
48
+ def searchable_on(*fields)
49
+
50
+ options = fields.last.kind_of?(Hash) ? fields.pop : {}
51
+ # TODO: handle options?
52
+
53
+ fields.each do |field|
54
+ if relation = self.reflections.keys.detect { |relation| field.to_s =~ Regexp.new("^#{relation}_(\\w+)$") }
55
+ scoped_search(:in => relation, :on => $1.to_sym)
56
+ else
57
+ scoped_search(:on => field)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # The default scoped_search exception class.
64
+ class Exception < StandardError
65
+ end
66
+
67
+ # The default exception class that is raised when there is something
68
+ # wrong with the scoped_search definition call.
69
+ #
70
+ # You usually do not want to catch this exception, but fix the faulty
71
+ # scoped_search method call.
72
+ class DefinitionError < ScopedSearch::Exception
73
+ end
74
+
75
+ # The default exception class that is raised when there is a problem
76
+ # with parsing or interpreting a search query.
77
+ #
78
+ # You may want to catch this exception and handle this gracefully.
79
+ class QueryNotSupported < ScopedSearch::Exception
80
+ end
81
+
82
+ end
83
+
84
+ # Load all lib files
85
+ require 'scoped_search/definition'
86
+ require 'scoped_search/query_language'
87
+ require 'scoped_search/query_builder'
88
+
89
+ # Import the search_on method in the ActiveReocrd::Base class
90
+ ActiveRecord::Base.send(:extend, ScopedSearch::ClassMethods)
91
+ ActiveRecord::Base.send(:extend, ScopedSearch::BackwardsCompatibility)
@@ -0,0 +1,145 @@
1
+ module ScopedSearch
2
+
3
+ # The ScopedSearch definition class defines on what fields should be search
4
+ # in the model in what cases
5
+ #
6
+ # A definition can be created by calling the <tt>scoped_search</tt> method on
7
+ # an ActiveRecord-based class, so you should not create an instance of this
8
+ # class yourself.
9
+ class Definition
10
+
11
+ # The Field class specifies a field of a model that is available for searching,
12
+ # in what cases this field should be searched and its default search behavior.
13
+ #
14
+ # Instances of this class are created when calling scoped_search in your model
15
+ # class, so you should not create instances of this class yourself.
16
+ class Field
17
+
18
+ attr_reader :definition, :field, :only_explicit, :relation
19
+
20
+ # The ActiveRecord-based class that belongs to this field.
21
+ def klass
22
+ if relation
23
+ definition.klass.reflections[relation].klass
24
+ else
25
+ definition.klass
26
+ end
27
+ end
28
+
29
+ # Returns the ActiveRecord column definition that corresponds to this field.
30
+ def column
31
+ klass.columns_hash[field.to_s]
32
+ end
33
+
34
+ # Returns the column type of this field.
35
+ def type
36
+ column.type
37
+ end
38
+
39
+ # Returns true if this field is a datetime-like column
40
+ def datetime?
41
+ [:datetime, :time, :timestamp].include?(type)
42
+ end
43
+
44
+ # Returns true if this field is a date-like column
45
+ def date?
46
+ type == :date
47
+ end
48
+
49
+ # Returns true if this field is a date or datetime-like column.
50
+ def temporal?
51
+ datetime? || date?
52
+ end
53
+
54
+ # Returns true if this field is numerical.
55
+ # Numerical means either integer, floating point or fixed point.
56
+ def numerical?
57
+ [:integer, :double, :float, :decimal].include?(type)
58
+ end
59
+
60
+ # Returns true if this is a textual column.
61
+ def textual?
62
+ [:string, :text].include?(type)
63
+ end
64
+
65
+ # Returns the default search operator for this field.
66
+ def default_operator
67
+ @default_operator ||= case type
68
+ when :string, :text then :like
69
+ else :eq
70
+ end
71
+ end
72
+
73
+ # Initializes a Field instance given the definition passed to the
74
+ # scoped_search call on the ActiveRecord-based model class.
75
+ def initialize(definition, options = {})
76
+ @definition = definition
77
+ case options
78
+ when Symbol, String
79
+ @field = field.to_sym
80
+ when Hash
81
+ @field = options.delete(:on)
82
+
83
+ # Set attributes from options hash
84
+ @relation = options[:in]
85
+ @only_explicit = !!options[:only_explicit]
86
+ @default_operator = options[:default_operator] if options.has_key?(:default_operator)
87
+ end
88
+
89
+ # Store this field is the field array
90
+ definition.fields[@field] ||= self
91
+ definition.unique_fields << self
92
+
93
+ # Store definition for alias / aliases as well
94
+ definition.fields[options[:alias]] ||= self if options[:alias]
95
+ options[:aliases].each { |al| definition.fields[al] ||= self } if options[:aliases]
96
+ end
97
+ end
98
+
99
+ attr_reader :klass, :fields, :unique_fields
100
+
101
+ # Initializes a ScopedSearch definition instance.
102
+ # This method will also setup a database adapter and create the :search_for
103
+ # named scope if it does not yet exist.
104
+ def initialize(klass)
105
+ @klass = klass
106
+ @fields = {}
107
+ @unique_fields = []
108
+
109
+ register_named_scope! unless klass.respond_to?(:search_for)
110
+ end
111
+
112
+ NUMERICAL_REGXP = /^\-?\d+(\.\d+)?$/
113
+
114
+ # Returns a list of appropriate fields to search in given a search keyword and operator.
115
+ def default_fields_for(value, operator = nil)
116
+
117
+ column_types = []
118
+ column_types += [:string, :text] if [nil, :like, :unlike, :ne, :eq].include?(operator)
119
+ column_types += [:integer, :double, :float, :decimal] if value =~ NUMERICAL_REGXP
120
+ column_types += [:datetime, :date, :timestamp] if (DateTime.parse(value) rescue nil)
121
+
122
+ default_fields.select { |field| column_types.include?(field.type) }
123
+ end
124
+
125
+ # Returns a list of fields that should be searched on by default.
126
+ #
127
+ # Every field will show up in this method's result, except for fields for
128
+ # which the only_explicit parameter is set to true.
129
+ def default_fields
130
+ unique_fields.reject { |field| field.only_explicit }
131
+ end
132
+
133
+ # Defines a new search field for this search definition.
134
+ def define(options)
135
+ Field.new(self, options)
136
+ end
137
+
138
+ protected
139
+
140
+ # Registers the search_for named scope within the class that is used for searching.
141
+ def register_named_scope! # :nodoc
142
+ @klass.named_scope(:search_for, lambda { |*args| ScopedSearch::QueryBuilder.build_query(args[1] || self, args[0]) })
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,296 @@
1
+ module ScopedSearch
2
+
3
+ # The QueryBuilder class builds an SQL query based on aquery string that is
4
+ # provided to the search_for named scope. It uses a SearchDefinition instance
5
+ # to shape the query.
6
+ class QueryBuilder
7
+
8
+ attr_reader :ast, :definition
9
+
10
+ # Creates a find parameter hash that can be passed to ActiveRecord::Base#find,
11
+ # given a search definition and query string. This method is called from the
12
+ # search_for named scope.
13
+ #
14
+ # This method will parse the query string and build an SQL query using the search
15
+ # query. It will return an ampty hash if the search query is empty, in which case
16
+ # the scope call will simply return all records.
17
+ def self.build_query(definition, query)
18
+ query_builder_class = self.class_for(definition)
19
+ if query.kind_of?(ScopedSearch::QueryLanguage::AST::Node)
20
+ return query_builder_class.new(definition, query).build_find_params
21
+ elsif query.kind_of?(String)
22
+ return query_builder_class.new(definition, ScopedSearch::QueryLanguage::Compiler.parse(query)).build_find_params
23
+ elsif query.nil?
24
+ return { }
25
+ else
26
+ raise "Unsupported query object: #{query.inspect}!"
27
+ end
28
+ end
29
+
30
+ # Loads the QueryBuilder class for the connection of the given definition.
31
+ # If no specific adapter is found, the default QueryBuilder class is returned.
32
+ def self.class_for(definition)
33
+ self.const_get(definition.klass.connection.class.name.split('::').last)
34
+ rescue
35
+ self
36
+ end
37
+
38
+ # Initializes the instance by setting the relevant parameters
39
+ def initialize(definition, ast)
40
+ @definition, @ast = definition, ast
41
+ end
42
+
43
+ # Actually builds the find parameters hash that should be used in the search_for
44
+ # named scope.
45
+ def build_find_params
46
+ parameters = []
47
+ includes = []
48
+
49
+ # Build SQL WHERE clause using the AST
50
+ sql = @ast.to_sql(self, definition) do |notification, value|
51
+
52
+ # Handle the notifications encountered during the SQL generation:
53
+ # Store the parameters, includes, etc so that they can be added to
54
+ # the find-hash later on.
55
+ case notification
56
+ when :parameter then parameters << value
57
+ when :include then includes << value
58
+ else raise ScopedSearch::QueryNotSupported, "Cannot handle #{notification.inspect}: #{value.inspect}"
59
+ end
60
+ end
61
+
62
+ # Build hash for ActiveRecord::Base#find for the named scope
63
+ find_attributes = {}
64
+ find_attributes[:conditions] = [sql] + parameters unless sql.nil?
65
+ find_attributes[:include] = includes.uniq unless includes.empty?
66
+ find_attributes # Uncomment for debugging
67
+ return find_attributes
68
+ end
69
+
70
+ # A hash that maps the operators of the query language with the corresponding SQL operator.
71
+ SQL_OPERATORS = { :eq =>'=', :ne => '<>', :like => 'LIKE', :unlike => 'NOT LIKE',
72
+ :gt => '>', :lt =>'<', :lte => '<=', :gte => '>=' }
73
+
74
+ # Return the SQL operator to use given an operator symbol and field definition.
75
+ #
76
+ # By default, it will simply look up the correct SQL operator in the SQL_OPERATORS
77
+ # hash, but this can be overrided by a database adapter.
78
+ def sql_operator(operator, field)
79
+ SQL_OPERATORS[operator]
80
+ end
81
+
82
+ # Perform a comparison between a field and a Date(Time) value.
83
+ #
84
+ # This function makes sure the date is valid and adjust the comparison in
85
+ # some cases to return more logical results.
86
+ #
87
+ # This function needs a block that can be used to pass other information about the query
88
+ # (parameters that should be escaped, includes) to the query builder.
89
+ #
90
+ # <tt>field</tt>:: The field to test.
91
+ # <tt>operator</tt>:: The operator used for comparison.
92
+ # <tt>value</tt>:: The value to compare the field with.
93
+ def datetime_test(field, operator, value, &block) # :yields: finder_option_type, value
94
+
95
+ # Parse the value as a date/time and ignore invalid timestamps
96
+ timestamp = parse_temporal(value)
97
+ return nil unless timestamp
98
+ timestamp = Date.parse(timestamp.strftime('%Y-%m-%d')) if field.date?
99
+
100
+ # Check for the case that a date-only value is given as search keyword,
101
+ # but the field is of datetime type. Change the comparison to return
102
+ # more logical results.
103
+ if timestamp.day_fraction == 0 && field.datetime?
104
+
105
+ if [:eq, :ne].include?(operator)
106
+ # Instead of looking for an exact (non-)match, look for dates that
107
+ # fall inside/outside the range of timestamps of that day.
108
+ yield(:parameter, timestamp)
109
+ yield(:parameter, timestamp + 1)
110
+ negate = (operator == :ne) ? 'NOT' : ''
111
+ field_sql = field.to_sql(operator, &block)
112
+ return "#{negate}(#{field_sql} >= ? AND #{field_sql} < ?)"
113
+
114
+ elsif operator == :gt
115
+ # Make sure timestamps on the given date are not included in the results
116
+ # by moving the date to the next day.
117
+ timestamp += 1
118
+ operator = :gte
119
+
120
+ elsif operator == :lte
121
+ # Make sure the timestamps of the given date are included by moving the
122
+ # date to the next date.
123
+ timestamp += 1
124
+ operator = :lt
125
+ end
126
+ end
127
+
128
+ # Yield the timestamp and return the SQL test
129
+ yield(:parameter, timestamp)
130
+ "#{field.to_sql(operator, &block)} #{sql_operator(operator, field)} ?"
131
+ end
132
+
133
+ # Generates a simple SQL test expression, for a field and value using an operator.
134
+ #
135
+ # This function needs a block that can be used to pass other information about the query
136
+ # (parameters that should be escaped, includes) to the query builder.
137
+ #
138
+ # <tt>field</tt>:: The field to test.
139
+ # <tt>operator</tt>:: The operator used for comparison.
140
+ # <tt>value</tt>:: The value to compare the field with.
141
+ def sql_test(field, operator, value, &block) # :yields: finder_option_type, value
142
+ if [:like, :unlike].include?(operator) && value !~ /^\%/ && value !~ /\%$/
143
+ yield(:parameter, "%#{value}%")
144
+ return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
145
+ elsif field.temporal?
146
+ return datetime_test(field, operator, value, &block)
147
+ else
148
+ yield(:parameter, value)
149
+ return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
150
+ end
151
+ end
152
+
153
+ # Try to parse a string as a datetime.
154
+ def parse_temporal(value)
155
+ DateTime.parse(value, true) rescue nil
156
+ end
157
+
158
+ # This module gets included into the Field class to add SQL generation.
159
+ module Field
160
+
161
+ # Return an SQL representation for this field. Also make sure that
162
+ # the relation which includes the search field is included in the
163
+ # SQL query.
164
+ #
165
+ # This function may yield an :include that should be used in the
166
+ # ActiveRecord::Base#find call, to make sure that the field is avalable
167
+ # for the SQL query.
168
+ def to_sql(builder, operator = nil, &block) # :yields: finder_option_type, value
169
+ yield(:include, relation) if relation
170
+ definition.klass.connection.quote_table_name(klass.table_name) + "." +
171
+ definition.klass.connection.quote_column_name(field)
172
+ end
173
+ end
174
+
175
+ # This module contains modules for every AST::Node class to add SQL generation.
176
+ module AST
177
+
178
+ # Defines the to_sql method for AST LeadNodes
179
+ module LeafNode
180
+ def to_sql(builder, definition, &block)
181
+ # Search keywords found without context, just search on all the default fields
182
+ fragments = definition.default_fields_for(value).map do |field|
183
+ builder.sql_test(field, field.default_operator, value, &block)
184
+ end
185
+ "(#{fragments.join(' OR ')})"
186
+ end
187
+ end
188
+
189
+ # Defines the to_sql method for AST operator nodes
190
+ module OperatorNode
191
+
192
+ # Returns a NOT(...) SQL fragment that negates the current AST node's children
193
+ def to_not_sql(builder, definition, &block)
194
+ "(NOT(#{rhs.to_sql(builder, definition, &block)}) OR #{rhs.to_sql(builder, definition, &block)} IS NULL)"
195
+ end
196
+
197
+ # Returns an IS (NOT) NULL SQL fragment
198
+ def to_null_sql(builder, definition, &block)
199
+ field = definition.fields[rhs.value.to_sym]
200
+ raise ScopedSearch::QueryNotSupported, "Field '#{rhs.value}' not recognized for searching!" unless field
201
+
202
+ case operator
203
+ when :null then "#{field.to_sql(builder, &block)} IS NULL"
204
+ when :notnull then "#{field.to_sql(builder, &block)} IS NOT NULL"
205
+ end
206
+ end
207
+
208
+ # No explicit field name given, run the operator on all default fields
209
+ def to_default_fields_sql(builder, definition, &block)
210
+ raise ScopedSearch::QueryNotSupported, "Value not a leaf node" unless rhs.kind_of?(ScopedSearch::QueryLanguage::AST::LeafNode)
211
+
212
+ # Search keywords found without context, just search on all the default fields
213
+ fragments = definition.default_fields_for(rhs.value, operator).map { |field|
214
+ builder.sql_test(field, operator, rhs.value, &block) }.compact
215
+
216
+ fragments.empty? ? nil : "(#{fragments.join(' OR ')})"
217
+ end
218
+
219
+ # Explicit field name given, run the operator on the specified field only
220
+ def to_single_field_sql(builder, definition, &block)
221
+ raise ScopedSearch::QueryNotSupported, "Field name not a leaf node" unless lhs.kind_of?(ScopedSearch::QueryLanguage::AST::LeafNode)
222
+ raise ScopedSearch::QueryNotSupported, "Value not a leaf node" unless rhs.kind_of?(ScopedSearch::QueryLanguage::AST::LeafNode)
223
+
224
+ # Search only on the given field.
225
+ field = definition.fields[lhs.value.to_sym]
226
+ raise ScopedSearch::QueryNotSupported, "Field '#{lhs.value}' not recognized for searching!" unless field
227
+ builder.sql_test(field, operator, rhs.value, &block)
228
+ end
229
+
230
+ # Convert this AST node to an SQL fragment.
231
+ def to_sql(builder, definition, &block)
232
+ if operator == :not && children.length == 1
233
+ to_not_sql(builder, definition, &block)
234
+ elsif [:null, :notnull].include?(operator)
235
+ to_null_sql(builder, definition, &block)
236
+ elsif children.length == 1
237
+ to_default_fields_sql(builder, definition, &block)
238
+ elsif children.length == 2
239
+ to_single_field_sql(builder, definition, &block)
240
+ else
241
+ raise ScopedSearch::QueryNotSupported, "Don't know how to handle this operator node: #{operator.inspect} with #{children.inspect}!"
242
+ end
243
+ end
244
+ end
245
+
246
+ # Defines the to_sql method for AST AND/OR operators
247
+ module LogicalOperatorNode
248
+ def to_sql(builder, definition, &block)
249
+ fragments = children.map { |c| c.to_sql(builder, definition, &block) }.compact
250
+ fragments.empty? ? nil : "(#{fragments.join(" #{operator.to_s.upcase} ")})"
251
+ end
252
+ end
253
+ end
254
+
255
+ # The MysqlAdapter makes sure that case sensitive comparisons are used
256
+ # when using the (not) equals operator, regardless of the field's
257
+ # collation setting.
258
+ class MysqlAdapter < ScopedSearch::QueryBuilder
259
+
260
+ # Patches the default <tt>sql_operator</tt> method to add
261
+ # <tt>BINARY</tt> after the equals and not equals operator to force
262
+ # case-sensitive comparisons.
263
+ def sql_operator(operator, field)
264
+ if [:ne, :eq].include?(operator) && field.textual?
265
+ "#{SQL_OPERATORS[operator]} BINARY"
266
+ else
267
+ super(operator, field)
268
+ end
269
+ end
270
+ end
271
+
272
+ # The PostgreSQLAdapter make sure that searches are case sensitive when
273
+ # using the like/unlike operators, by using the PostrgeSQL-specific
274
+ # <tt>ILIKE operator</tt> instead of <tt>LIKE</tt>.
275
+ class PostgreSQLAdapter < ScopedSearch::QueryBuilder
276
+
277
+ # Switches out the default LIKE operator for ILIKE in the default
278
+ # <tt>sql_operator</tt> method.
279
+ def sql_operator(operator, field)
280
+ case operator
281
+ when :like then 'ILIKE'
282
+ when :unlike then 'NOT ILIKE'
283
+ else super(operator, field)
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ # Include the modules into the corresponding classes
290
+ # to add SQL generation capabilities to them.
291
+
292
+ Definition::Field.send(:include, QueryBuilder::Field)
293
+ QueryLanguage::AST::LeafNode.send(:include, QueryBuilder::AST::LeafNode)
294
+ QueryLanguage::AST::OperatorNode.send(:include, QueryBuilder::AST::OperatorNode)
295
+ QueryLanguage::AST::LogicalOperatorNode.send(:include, QueryBuilder::AST::LogicalOperatorNode)
296
+ end