| Class | SearchApi::Bridge::ActiveRecord |
| In: |
lib/active_record_bridge.rb
|
| Parent: | Base |
SearchApi::Bridge::Base subclass that allows ActiveRecord to be used with SearchApi::Search::Base.
| SINGLE_COLUMN_OPERATORS | = | %w(eq neq lt lte gt gte contains starts_with ends_with) | Operators that apply on a single column. | |
| MULTI_COLUMN_OPERATORS | = | %w(full_text) | Operators that apply on several columns. | |
| VALID_FIND_OPTIONS | = | [ :conditions, :include, :joins, :order, :select, :group, :having ] |
This method is called when a SearchApi::Search::Base‘s model is set, in order to predefine some relevant search keys.
Returns an Array of SearchApi::Search::SearchAttributeBuilder instances.
Each builder can be used as an argument for SearchApi::Search::Base.search_accessor.
In the contexte of ActiveRecord:
With the same name as the column, it has the exact same behavior as the standard AR::Base.find(:all, :conditions => {column => value}).
Valid options are:
Example
class Search1 < SearchApi::Search::Base
model Searchable
end
class Search2 < SearchApi::Search::Base
model Searchable, :type_cast => true
end
search1 = Search1.new
search2 = Search2.new
search1.id = search2.id = '12'
search1.id => '12' # no type cast
search2.id => 12 # type cast in action
search1.min_id = search2.min_id = '12' # OK, predefined search attribute for numeric column
search1.max_id = search2.max_id = '12' # OK, predefined search attribute for numeric column
# File lib/active_record_bridge.rb, line 73
73: def automatic_search_attribute_builders(options)
74:
75: # every column will create builders
76: builders = []
77: @active_record_class.columns.each do |column|
78:
79: # Append a builder for a standard AR::Base search.
80: builders << ::SearchApi::Search::SearchAttributeBuilder.new(
81: column.name, # search attribute name is the column name,
82: :type_cast => options[:type_cast], # type cast if required,
83: :column => column.name, # look in to that very column...
84: :operator => :eq) # ... for equality
85:
86: # Create extra builders for comparable columns
87: if column.klass < Comparable
88: # Builder for a lower-bound search
89: builders << ::SearchApi::Search::SearchAttributeBuilder.new(
90: "min_#{column.name}", # search attribute name is min_column name,
91: :type_cast => options[:type_cast], # type cast if required,
92: :column => column.name, # look in to that very column...
93: :operator => :gte) # ... for values greater or equal to lower bound
94:
95: # Builder for a upper-bound search
96: builders << ::SearchApi::Search::SearchAttributeBuilder.new(
97: "max_#{column.name}", # search attribute name is max_column name,
98: :type_cast => options[:type_cast], # type cast if required,
99: :column => column.name, # look in to that very column...
100: :operator => :lte) # ... for values lower or equal to upper bound
101: end
102: end
103: builders
104: end
Overrides default Bridge::Base.merge_find_options.
This methods returns a merge of options in options_array.
# File lib/active_record_bridge.rb, line 265
265: def merge_find_options(options_array)
266: all_options = options_array.compact.inject({}) do |all_options, options|
267: self.class.validate_find_options(options)
268: options.each do |key, value|
269: next if value.blank? || (value.respond_to?(:empty?) && value.empty?)
270: (all_options[key] ||= []) << value
271: end
272: all_options
273: end
274:
275:
276: merged_options = {}
277:
278:
279: # Merge :conditions options
280:
281: unless all_options[:conditions].nil? || all_options[:conditions].empty?
282: # merge conditions with AND
283: merged_options[:conditions] = '(' + all_options[:conditions].
284: map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
285: uniq.
286: join(") AND (")+ ')'
287: end
288:
289:
290: # Merge :include options
291:
292: unless all_options[:include].nil? || all_options[:include].empty?
293: # merge includes with set-union
294: merged_options[:include] = all_options[:include].inject([]) { |merged_includes, include_options| merged_includes |= Array(include_options) }
295: end
296:
297:
298: # Merge :joins options
299:
300: unless all_options[:joins].nil? || all_options[:joins].empty?
301: # merge joins with space
302: merged_options[:joins] = all_options[:joins].
303: map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
304: uniq.
305: join(' ')
306: end
307:
308:
309: # Merge :groups and :having options
310:
311: unless all_options[:groups].nil? || all_options[:groups].empty?
312: # merge groups with comma
313: merged_options[:group] = all_options[:groups].
314: map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
315: uniq.
316: join(', ')
317:
318: # merge having conditions into :group option
319: unless all_options[:having].nil? || all_options[:having].empty?
320: # merge having with AND
321: merged_options[:group] += ' HAVING (' + all_options[:having].
322: map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
323: uniq.
324: join(') AND (')+ ')'
325: end
326: end
327:
328:
329: # Merge :order options
330:
331: unless all_options[:order].nil? || all_options[:order].empty?
332: # merge order with comma
333: merged_options[:order] = all_options[:order].
334: map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
335: join(', ')
336: end
337:
338:
339: # Merge :select options
340:
341: unless all_options[:select].nil? || all_options[:select].empty?
342: # merge select with comma
343: merged_options[:select] = all_options[:select].
344: map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
345: uniq.
346: join(', ')
347: end
348:
349: if merged_options[:joins] && merged_options[:select].nil?
350: # since joins add columns, restrict default column set to base class columns
351: merged_options[:select] = "DISTINCT #{@active_record_class.table_name}.*"
352: end
353:
354:
355: # merged_options is now ready for ActiveRecord::Base
356:
357: merged_options
358: end
This method is called when a SearchApi::Search::Base.search_accessor is called, to help you implementing some usual ActiveRecord searches.
Modifies in place a SearchApi::Search::SearchAttributeBuilder.
On output, search_attribute_builder should be a valid SearchApi::Search::Base.add_search_attribute argument.
You may provide an :operator option.
Some apply on a single column, other on several ones.
Single-column operator are:
It has the exact same behavior as the standard AR::Base.find(:all, :conditions => {column => value}).
Multi-column operators are:
Those operators require some other options:
# File lib/active_record_bridge.rb, line 143
143: def rewrite_search_attribute_builder(search_attribute_builder)
144: # consume :operator option
145: operator = search_attribute_builder.options.delete(:operator)
146: return unless operator
147:
148: if SINGLE_COLUMN_OPERATORS.include?(operator.to_s)
149:
150: search_attribute = search_attribute_builder.name
151: options = search_attribute_builder.options
152:
153: # consume :column option
154: column_name = options.delete(:column)
155: raise ArgumentError.new("#{operator} operator requires the :column options to contain a column name.") unless column_name && !column_name.is_a?(Array)
156:
157: # we'll use that column name everywhere
158: sql_column_name = "#{@active_record_class.table_name}.#{@active_record_class.connection.quote_column_name(column_name)}"
159:
160: # consume :type_cast option
161: if options.delete(:type_cast)
162: @active_record_instance ||= @active_record_class.new
163: # §§§ what if :store_as option is already defined ?
164: options[:store_as] = proc do |value|
165: @active_record_instance.send("#{column_name}=", value)
166: @active_record_instance.send(column_name)
167: end
168: end
169:
170: # block rewriting
171: case operator
172: when :eq
173: search_attribute_builder.block = proc do |search|
174: { :conditions => search.class.model.send(:sanitize_sql_hash, column_name => search.send(search_attribute)) }
175: end
176:
177: when :neq
178: # §§§ some work is necessary on boolean columns
179: search_attribute_builder.block = proc do |search|
180: case value = search.send(search_attribute)
181: when nil
182: { :conditions => "#{sql_column_name} IS NOT NULL" }
183: else
184: { :conditions => ["#{sql_column_name} <> ? OR #{sql_column_name} IS NULL", value] }
185: end
186: end
187:
188: when :lt
189: search_attribute_builder.block = proc do |search|
190: value = search.send(search_attribute)
191: { :conditions => ["#{sql_column_name} < ?", value] } unless value.nil?
192: end
193:
194: when :lte
195: search_attribute_builder.block = proc do |search|
196: value = search.send(search_attribute)
197: { :conditions => ["#{sql_column_name} <= ?", value] } unless value.nil?
198: end
199:
200: when :gt
201: search_attribute_builder.block = proc do |search|
202: value = search.send(search_attribute)
203: { :conditions => ["#{sql_column_name} > ?", value] } unless value.nil?
204: end
205:
206: when :gte
207: search_attribute_builder.block = proc do |search|
208: value = search.send(search_attribute)
209: { :conditions => ["#{sql_column_name} >= ?", value] } unless value.nil?
210: end
211:
212: when :contains
213: search_attribute_builder.block = proc do |search|
214: value = search.send(search_attribute).to_s
215: { :conditions => ["#{sql_column_name} LIKE ?", "%#{value}%"] } unless value.empty?
216: end
217:
218: when :starts_with
219: search_attribute_builder.block = proc do |search|
220: value = search.send(search_attribute).to_s
221: { :conditions => ["#{sql_column_name} LIKE ?", "#{search.send(search_attribute)}%"] } unless value.empty?
222: end
223:
224: when :ends_with
225: search_attribute_builder.block = proc do |search|
226: value = search.send(search_attribute).to_s
227: { :conditions => ["#{sql_column_name} LIKE ?", "%#{search.send(search_attribute)}"] } unless value.empty?
228: end
229: end
230:
231: elsif MULTI_COLUMN_OPERATORS.include?(operator.to_s)
232:
233: search_attribute = search_attribute_builder.name
234: options = search_attribute_builder.options
235:
236: # consume :columns || :column option
237: column_names = Array(options.delete(:columns) || options.delete(:column))
238: raise ArgumentError.new("#{operator} operator requires the :column or :columns options to contain column names.") if column_names.empty?
239:
240: # we'll use that column names everywhere
241: sql_column_names = column_names.map do |column_name|
242: "#{@active_record_class.table_name}.#{@active_record_class.connection.quote_column_name(column_name)}"
243: end
244:
245: case operator
246: when :full_text
247: # We'll use TextCriterion class.
248:
249: # consume :exclude option
250: exclude = options.delete(:exclude) || /^[^0-9].{0,2}$/
251:
252: search_attribute_builder.block = lambda do |search|
253: value = search.send(search_attribute).to_s
254: { :conditions => TextCriterion.new(value, :exclude => exclude).condition(sql_column_names) } unless value.empty?
255: end
256: end
257: else
258: raise ArgumentError.new("Unknown operator #{operator}")
259: end
260: end