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.

Methods

Constants

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 ]

Public Instance methods

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:

  • each columns defines at least one search attribute, the obvious equality search attribute.

    With the same name as the column, it has the exact same behavior as the standard AR::Base.find(:all, :conditions => {column => value}).

  • each comparable column defines a lower and an upper-bound search attribute, named min_xxx and max_xxx when xxx is the column name.

Valid options are:

  • :type_cast - default false: when true, returned builders will use the :store_as option in order to type cast search attributes according to column type.

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

[Source]

     # 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.

[Source]

     # 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:

  • :eq - equality operator.

    It has the exact same behavior as the standard AR::Base.find(:all, :conditions => {column => value}).

  • :neq - inequality operator
  • :lt - "lower than" operator
  • :lte - "lower than or equal" operator
  • :gt - "greater than" operator
  • :gte - "greater than or equal" operator
  • :contains - uses LIKE sql operator
  • :starts_with - uses LIKE sql operator
  • :ends_with - uses LIKE sql operator

Multi-column operators are:

  • :full_text - full text search

Those operators require some other options:

  • :column - required by single column operator
  • :columns - required by multi column operator
  • :type_cast - optional for single column operators, default false. When true, search_attribute_builder is rewritten so that its :store_as option casts incoming values according to column type.

[Source]

     # 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

[Validate]