albanpeignier-searchapi 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/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