albanpeignier-searchapi 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Gwendal Roué, Pierlis
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/Manifest.txt ADDED
@@ -0,0 +1,25 @@
1
+ MIT-LICENSE
2
+ Manifest.txt
3
+ README
4
+ Rakefile
5
+ db/migrate/001_create_searchable.rb
6
+ init.rb
7
+ install.rb
8
+ lib/search_api.rb
9
+ lib/search_api/active_record_bridge.rb
10
+ lib/search_api/active_record_integration.rb
11
+ lib/search_api/bridge.rb
12
+ lib/search_api/callbacks.rb
13
+ lib/search_api/errors.rb
14
+ lib/search_api/search.rb
15
+ lib/search_api/sql_fragment.rb
16
+ lib/search_api/text_criterion.rb
17
+ searchapi.gemspec
18
+ tasks/search_api_tasks.rake
19
+ test/active_record_bridge_test.rb
20
+ test/active_record_integration_test.rb
21
+ test/bridge_test.rb
22
+ test/callbacks_test.rb
23
+ test/mock_model.rb
24
+ test/search_test.rb
25
+ uninstall.rb
data/README ADDED
@@ -0,0 +1,98 @@
1
+ == Download
2
+
3
+ The latest version of SearchAPI can be found at
4
+
5
+ * http://rubyforge.org/projects/searchapi
6
+
7
+ Documentation can be found at
8
+
9
+ * http://www.pierlis.com/doc/searchapi
10
+
11
+
12
+ == Installation
13
+
14
+ The preferred method of installing SearchAPI is through the following command:
15
+
16
+ $ script/plugin install svn://rubyforge.org/var/svn/searchapi
17
+
18
+
19
+ == License
20
+
21
+ SearchAPI is released under the MIT license.
22
+
23
+
24
+ == SearchApi
25
+
26
+ Look at following Rails expression, which look for 34 years-old men:
27
+
28
+ Person.find(
29
+ :all,
30
+ :conditions => {:sex => 'M',
31
+ :birth_date => (Date.today-34.years)..
32
+ (Date.today-33.years+1.day))
33
+
34
+ That's a pretty handy way to avoid using heavy SQL expressions like:
35
+
36
+ Person.find(
37
+ :all,
38
+ :conditions => ['sex = ? AND birth_date BETWEEN ? AND ?,
39
+ 'M',
40
+ (Date.today-34.years),
41
+ (Date.today-33.years+1.day)])
42
+
43
+ SearchApi plugin pushes the concept a step further, allowing you to define custom search keys that you can use in these condition hashes:
44
+
45
+ Person.find(
46
+ :all,
47
+ :conditions => { :male => true, :age => 34 })
48
+
49
+
50
+ Or, why not:
51
+
52
+ Person.find(
53
+ :all,
54
+ :conditions => { :thirty_four_aged_men => true })
55
+
56
+ This last expression would return people matching whatever condition is held by the "thirty_four_aged_men" concept.
57
+
58
+ <b>SearchApi allows for defining Search API through SQL encapsulation</b>, thanks to those keys in conditions hashes that are decoupled from actual underlying columns.
59
+
60
+ === Example
61
+
62
+ Let's define the <tt>:male</tt> and <tt>:age</tt> search keys:
63
+
64
+ class Person < ActiveRecord::Base
65
+ has_search_api
66
+
67
+ # define age search key
68
+ search :age do |search|
69
+ { :conditions => ['birth_date BETWEEN ? AND ?',
70
+ (Date.today-search.age.years),
71
+ (Date.today-(search.age-1).years+1.day)]}
72
+ end
73
+
74
+ # define male search key
75
+ search :male do |search|
76
+ { :conditions => ['sex = ?', if search.male then 'M' else 'F' end]}
77
+ end
78
+ end
79
+
80
+ === Navigate in this documentation
81
+
82
+ - <b>Learn how to add your own search keys</b>
83
+
84
+ Jump directly to the documentation of ActiveRecord::Base and its has_search_api method.
85
+
86
+ - <b>Learn about which search keys are automatically defined</b>
87
+
88
+ When your model calls has_search_api, many handy search keys are automatically defined: go look at SearchApi::Bridge::ActiveRecord and its method automatic_search_attribute_builders.
89
+
90
+ - <b>Dig further into SearchApi plugin</b>
91
+
92
+ Learn about:
93
+
94
+ - <b>bridges</b>: SearchApi::Bridge::Base allows any class to be searchable;
95
+
96
+ - <b>ActiveRecord bridge</b>: SearchApi::Bridge::ActiveRecord implements ActiveRecord searchable capabilities;
97
+
98
+ - <b>ActiveRecord integration</b>: SearchApi::Integration::ActiveRecord that ties all together, allowing you to extend conditions hashes.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => :test
8
+
9
+ desc 'Test the searchapi plugin.'
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the search_api plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'SearchApi'
20
+ rdoc.options << '--line-numbers' << '--inline-source' << '-c utf-8'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ %w[rubygems hoe].each { |f| require f }
26
+ # Generate all the Rake tasks
27
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
28
+ $hoe = Hoe.new('searchapi', '0.1') do |p|
29
+ p.developer("Gwendal Roué", "gr@pierlis.com")
30
+ p.summary = "Ruby on Rails plugin which purpose is to let the developper define Search APIs for ActiveRecord models"
31
+ p.rubyforge_name = p.name # TODO this is default value
32
+ p.extra_deps = [['activerecord']]
33
+
34
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
35
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
36
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
37
+ p.rsync_args = '-av --delete --ignore-errors'
38
+ end
39
+
40
+ desc 'Recreate Manifest.txt to include ALL files'
41
+ task :manifest do
42
+ `rake check_manifest | patch -p0 > Manifest.txt`
43
+ end
44
+
45
+ desc "Generate a #{$hoe.name}.gemspec file"
46
+ task :gemspec do
47
+ File.open("#{$hoe.name}.gemspec", "w") do |file|
48
+ file.puts $hoe.spec.to_ruby
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../test/mock_model'))
2
+
3
+ class CreateSearchable < ActiveRecord::Migration
4
+ def self.up
5
+ create_table :searchables do |t|
6
+ t.column :age, :integer
7
+ t.column :name, :string
8
+ t.column :city, :string
9
+ t.column :funny, :boolean, :default => true
10
+ end
11
+
12
+ # fill in plenty of random records
13
+
14
+ 1000.times do
15
+ Searchable.create(
16
+ :age => case age = rand(100)
17
+ when 0; nil
18
+ else age
19
+ end,
20
+ :name => (%w(John Mary Bob Andy Sylvia Marc Ann Mary-Ann)+[nil])[rand(9)],
21
+ :city => (%w(Paris London Berlin Madrid Roma Budapest Bruxelles Lisboa)+[nil])[rand(9)],
22
+ :funny => case rand(3)
23
+ when 0; nil
24
+ when 1: true
25
+ when 2: false
26
+ end)
27
+ end
28
+ end
29
+
30
+ def self.down
31
+ drop_table :searchables
32
+ end
33
+ end
data/init.rb ADDED
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ #--
3
+ # Copyright (c) 2007 Gwendal Roué, Pierlis
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #++
24
+
25
+ require 'search_api'
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
data/lib/search_api.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'search_api/errors'
2
+ require 'search_api/search'
3
+ require 'search_api/callbacks'
4
+ require 'search_api/bridge'
5
+ require 'search_api/sql_fragment'
6
+ require 'search_api/text_criterion'
7
+ require 'search_api/active_record_bridge'
8
+ require 'search_api/active_record_integration'
9
+
10
+
11
+ class SearchApi::Search::Base
12
+ include SearchApi::Search::Callbacks
13
+ end
@@ -0,0 +1,385 @@
1
+ require 'search_api'
2
+
3
+ module SearchApi
4
+ module Bridge
5
+
6
+ # SearchApi::Bridge::Base subclass that allows ActiveRecord to be used with SearchApi::Search::Base.
7
+
8
+ class ActiveRecord < Base
9
+
10
+ # Operators that apply on a single column.
11
+ SINGLE_COLUMN_OPERATORS = %w(eq neq lt lte gt gte contains starts_with ends_with)
12
+
13
+ # Operators that apply on several columns.
14
+ MULTI_COLUMN_OPERATORS = %w(full_text)
15
+
16
+ class << self
17
+ VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :order, :select, :group, :having ]
18
+
19
+ def validate_find_options(options) #:nodoc:
20
+ options.assert_valid_keys(VALID_FIND_OPTIONS)
21
+ end
22
+ end
23
+
24
+ # store the active_record_subclass
25
+ def initialize(active_record_subclass) #:nodoc:
26
+ @active_record_class = active_record_subclass
27
+ end
28
+
29
+ # This method is called when a SearchApi::Search::Base's model is set,
30
+ # in order to predefine some relevant search keys.
31
+ #
32
+ # Returns an Array of SearchApi::Search::SearchAttributeBuilder instances.
33
+ #
34
+ # Each builder can be used as an argument for SearchApi::Search::Base.search_accessor.
35
+ #
36
+ # In the contexte of ActiveRecord:
37
+ # - each columns defines at least one search attribute, the obvious
38
+ # equality search attribute.
39
+ #
40
+ # With the same name as the column, it has the exact same behavior as
41
+ # the standard <tt>AR::Base.find(:all, :conditions => {column => value})</tt>.
42
+ #
43
+ # - each comparable column defines a lower and an upper-bound search attribute,
44
+ # named min_xxx and max_xxx when xxx is the column name.
45
+ #
46
+ #
47
+ # Valid options are:
48
+ # - <tt>:type_cast</tt> - default false: when true, returned builders will
49
+ # use the <tt>:store_as</tt> option in order to type cast search attributes
50
+ # according to column type.
51
+ #
52
+ #
53
+ # Example
54
+ #
55
+ # class Search1 < SearchApi::Search::Base
56
+ # model Searchable
57
+ # end
58
+ #
59
+ # class Search2 < SearchApi::Search::Base
60
+ # model Searchable, :type_cast => true
61
+ # end
62
+ #
63
+ # search1 = Search1.new
64
+ # search2 = Search2.new
65
+ #
66
+ # search1.id = search2.id = '12'
67
+ # search1.id => '12' # no type cast
68
+ # search2.id => 12 # type cast in action
69
+ #
70
+ # search1.min_id = search2.min_id = '12' # OK, predefined search attribute for numeric column
71
+ # search1.max_id = search2.max_id = '12' # OK, predefined search attribute for numeric column
72
+
73
+ def automatic_search_attribute_builders(options)
74
+
75
+ # every column will create builders
76
+ columns = @active_record_class.columns rescue [] # if no column can be found, there may be a database problem.
77
+
78
+ builders = []
79
+ columns.each do |column|
80
+
81
+ # Append a builder for a standard AR::Base search.
82
+ builders << ::SearchApi::Search::SearchAttributeBuilder.new(
83
+ column.name, # search attribute name is the column name,
84
+ :type_cast => options[:type_cast], # type cast if required,
85
+ :column => column.name, # look in to that very column...
86
+ :operator => :eq) # ... for equality
87
+
88
+ # Create extra builders for comparable columns
89
+ if column.klass < Comparable
90
+ # Builder for a lower-bound search
91
+ builders << ::SearchApi::Search::SearchAttributeBuilder.new(
92
+ "min_#{column.name}", # search attribute name is min_column name,
93
+ :type_cast => options[:type_cast], # type cast if required,
94
+ :column => column.name, # look in to that very column...
95
+ :operator => :gte) # ... for values greater or equal to lower bound
96
+
97
+ # Builder for a upper-bound search
98
+ builders << ::SearchApi::Search::SearchAttributeBuilder.new(
99
+ "max_#{column.name}", # search attribute name is max_column name,
100
+ :type_cast => options[:type_cast], # type cast if required,
101
+ :column => column.name, # look in to that very column...
102
+ :operator => :lte) # ... for values lower or equal to upper bound
103
+ end
104
+ end
105
+ builders
106
+ end
107
+
108
+
109
+ # This method is called when a SearchApi::Search::Base.search_accessor is
110
+ # called, to help you implementing some usual ActiveRecord searches.
111
+ #
112
+ # Modifies in place a SearchApi::Search::SearchAttributeBuilder.
113
+ #
114
+ # On output, search_attribute_builder should be a valid
115
+ # SearchApi::Search::Base.add_search_attribute argument.
116
+ #
117
+ # You may provide an <tt>:operator</tt> option.
118
+ #
119
+ # Some apply on a single column, other on several ones.
120
+ #
121
+ # Single-column operator are:
122
+ # - <tt>:eq</tt> - equality operator.
123
+ #
124
+ # It has the exact same behavior as the standard
125
+ # <tt>AR::Base.find(:all, :conditions => {column => value})</tt>.
126
+ #
127
+ # - <tt>:neq</tt> - inequality operator
128
+ # - <tt>:lt</tt> - "lower than" operator
129
+ # - <tt>:lte</tt> - "lower than or equal" operator
130
+ # - <tt>:gt</tt> - "greater than" operator
131
+ # - <tt>:gte</tt> - "greater than or equal" operator
132
+ # - <tt>:contains</tt> - uses LIKE sql operator
133
+ # - <tt>:starts_with</tt> - uses LIKE sql operator
134
+ # - <tt>:ends_with</tt> - uses LIKE sql operator
135
+ #
136
+ # Multi-column operators are:
137
+ # - <tt>:full_text</tt> - full text search
138
+ #
139
+ # Those operators require some other options:
140
+ # - <tt>:column</tt> - required by single column operator
141
+ # - <tt>:columns</tt> - required by multi column operator
142
+ # - <tt>:type_cast</tt> - optional for single column operators, default false.
143
+ # When true, search_attribute_builder is rewritten so that its
144
+ # <tt>:store_as</tt> option casts incoming values according to column type.
145
+ def rewrite_search_attribute_builder(search_attribute_builder)
146
+ # consume :operator option
147
+ operator = search_attribute_builder.options.delete(:operator)
148
+ return unless operator
149
+
150
+ if SINGLE_COLUMN_OPERATORS.include?(operator.to_s)
151
+
152
+ search_attribute = search_attribute_builder.name
153
+ options = search_attribute_builder.options
154
+
155
+ # consume :column option
156
+ column_name = options.delete(:column)
157
+ raise ArgumentError.new("#{operator} operator requires the :column options to contain a column name.") unless column_name && !column_name.is_a?(Array)
158
+
159
+ # we'll use that column name everywhere
160
+ sql_column_name = "#{@active_record_class.table_name}.#{@active_record_class.connection.quote_column_name(column_name)}"
161
+
162
+ # consume :type_cast option
163
+ if options.delete(:type_cast)
164
+ @active_record_instance ||= @active_record_class.new
165
+ # §§§ what if :store_as option is already defined ?
166
+ options[:store_as] = proc do |value|
167
+ @active_record_instance.send("#{column_name}=", value)
168
+ @active_record_instance.send(column_name)
169
+ end
170
+ end
171
+
172
+ # block rewriting
173
+ case operator
174
+ when :eq
175
+ search_attribute_builder.block = proc do |search|
176
+ { :conditions => search.class.model.send(:sanitize_sql_hash, column_name => search.send(search_attribute)) }
177
+ end
178
+
179
+ when :neq
180
+ # §§§ some work is necessary on boolean columns
181
+ search_attribute_builder.block = proc do |search|
182
+ case value = search.send(search_attribute)
183
+ when nil
184
+ { :conditions => "#{sql_column_name} IS NOT NULL" }
185
+ else
186
+ { :conditions => ["#{sql_column_name} <> ? OR #{sql_column_name} IS NULL", value] }
187
+ end
188
+ end
189
+
190
+ when :lt
191
+ search_attribute_builder.block = proc do |search|
192
+ value = search.send(search_attribute)
193
+ { :conditions => ["#{sql_column_name} < ?", value] } unless value.nil?
194
+ end
195
+
196
+ when :lte
197
+ search_attribute_builder.block = proc do |search|
198
+ value = search.send(search_attribute)
199
+ { :conditions => ["#{sql_column_name} <= ?", value] } unless value.nil?
200
+ end
201
+
202
+ when :gt
203
+ search_attribute_builder.block = proc do |search|
204
+ value = search.send(search_attribute)
205
+ { :conditions => ["#{sql_column_name} > ?", value] } unless value.nil?
206
+ end
207
+
208
+ when :gte
209
+ search_attribute_builder.block = proc do |search|
210
+ value = search.send(search_attribute)
211
+ { :conditions => ["#{sql_column_name} >= ?", value] } unless value.nil?
212
+ end
213
+
214
+ when :contains
215
+ search_attribute_builder.block = proc do |search|
216
+ value = search.send(search_attribute).to_s
217
+ { :conditions => ["#{sql_column_name} LIKE ?", "%#{value}%"] } unless value.empty?
218
+ end
219
+
220
+ when :starts_with
221
+ search_attribute_builder.block = proc do |search|
222
+ value = search.send(search_attribute).to_s
223
+ { :conditions => ["#{sql_column_name} LIKE ?", "#{search.send(search_attribute)}%"] } unless value.empty?
224
+ end
225
+
226
+ when :ends_with
227
+ search_attribute_builder.block = proc do |search|
228
+ value = search.send(search_attribute).to_s
229
+ { :conditions => ["#{sql_column_name} LIKE ?", "%#{search.send(search_attribute)}"] } unless value.empty?
230
+ end
231
+ end
232
+
233
+ elsif MULTI_COLUMN_OPERATORS.include?(operator.to_s)
234
+
235
+ search_attribute = search_attribute_builder.name
236
+ options = search_attribute_builder.options
237
+
238
+ # consume :columns || :column option
239
+ column_names = Array(options.delete(:columns) || options.delete(:column))
240
+ raise ArgumentError.new("#{operator} operator requires the :column or :columns options to contain column names.") if column_names.empty?
241
+
242
+ # we'll use that column names everywhere
243
+ sql_column_names = column_names.map do |column_name|
244
+ "#{@active_record_class.table_name}.#{@active_record_class.connection.quote_column_name(column_name)}"
245
+ end
246
+
247
+ case operator
248
+ when :full_text
249
+ # We'll use TextCriterion class.
250
+
251
+ # consume :exclude option
252
+ exclude = options.delete(:exclude) || /^[^0-9].{0,2}$/
253
+
254
+ search_attribute_builder.block = lambda do |search|
255
+ value = search.send(search_attribute).to_s
256
+ { :conditions => TextCriterion.new(value, :exclude => exclude).condition(sql_column_names) } unless value.empty?
257
+ end
258
+ end
259
+ else
260
+ raise ArgumentError.new("Unknown operator #{operator}")
261
+ end
262
+ end
263
+
264
+ # Overrides default Bridge::Base.merge_find_options.
265
+ #
266
+ # This methods returns a merge of options in options_array.
267
+ def merge_find_options(options_array)
268
+ all_options = options_array.compact.inject({}) do |all_options, options|
269
+ self.class.validate_find_options(options)
270
+ options.each do |key, value|
271
+ next if value.blank? || (value.respond_to?(:empty?) && value.empty?)
272
+ (all_options[key] ||= []) << value
273
+ end
274
+ all_options
275
+ end
276
+
277
+
278
+ merged_options = {}
279
+
280
+
281
+ # Merge :conditions options
282
+
283
+ unless all_options[:conditions].nil? || all_options[:conditions].empty?
284
+ # merge conditions with AND
285
+ merged_options[:conditions] = '(' + all_options[:conditions].
286
+ map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
287
+ uniq.
288
+ join(") AND (")+ ')'
289
+ end
290
+
291
+
292
+ # Merge :include options
293
+
294
+ unless all_options[:include].nil? || all_options[:include].empty?
295
+ # merge includes with set-union
296
+ merged_options[:include] = all_options[:include].inject([]) { |merged_includes, include_options| merged_includes |= Array(include_options) }
297
+ end
298
+
299
+
300
+ # Merge :joins options
301
+
302
+ unless all_options[:joins].nil? || all_options[:joins].empty?
303
+ # merge joins with space
304
+ merged_options[:joins] = all_options[:joins].
305
+ map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
306
+ uniq.
307
+ join(' ')
308
+ end
309
+
310
+
311
+ # Merge :group and :having options
312
+
313
+ unless all_options[:having].nil? || all_options[:having].empty?
314
+ # default group by if having clause is present
315
+ if all_options[:group].nil? || all_options[:group].empty?
316
+ all_options[:group] = ["#{@active_record_class.table_name}.#{@active_record_class.primary_key}"]
317
+ end
318
+ end
319
+
320
+ unless all_options[:group].nil? || all_options[:group].empty?
321
+ # merge groups with comma
322
+ merged_options[:group] = all_options[:group].
323
+ map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
324
+ uniq.
325
+ join(', ')
326
+
327
+ # merge having conditions into :group option
328
+ unless all_options[:having].nil? || all_options[:having].empty?
329
+ # merge having with AND
330
+ merged_options[:group] += ' HAVING (' + all_options[:having].
331
+ map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
332
+ uniq.
333
+ join(') AND (')+ ')'
334
+ end
335
+ end
336
+
337
+
338
+ # Merge :order options
339
+
340
+ unless all_options[:order].nil? || all_options[:order].empty?
341
+ # merge order with comma
342
+ merged_options[:order] = all_options[:order].
343
+ map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
344
+ join(', ')
345
+ end
346
+
347
+
348
+ # Merge :select options
349
+
350
+ unless all_options[:select].nil? || all_options[:select].empty?
351
+ # merge select with comma
352
+ merged_options[:select] = all_options[:select].
353
+ map { |fragment| SearchApi::SqlFragment.sanitize(fragment) }.
354
+ uniq.
355
+ join(', ')
356
+ end
357
+
358
+ if merged_options[:joins] && merged_options[:select].nil?
359
+ # since joins add columns, restrict default column set to base class columns
360
+ merged_options[:select] = "DISTINCT #{@active_record_class.table_name}.*"
361
+ end
362
+
363
+
364
+ # merged_options is now ready for ActiveRecord::Base
365
+
366
+ merged_options
367
+ end
368
+ end
369
+
370
+ end
371
+ end
372
+
373
+
374
+ class ActiveRecord::Base
375
+ class << self
376
+
377
+ # Returns an SearchApi::Bridge::ActiveRecord instance.
378
+ #
379
+ # The presence of this method allows ActiveRecord::Base subclasses
380
+ # to be used as models by SearchApi::Search::Base subclasses.
381
+ def search_api_bridge
382
+ SearchApi::Bridge::ActiveRecord.new(self)
383
+ end
384
+ end
385
+ end