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