albanpeignier-searchapi 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/Manifest.txt +25 -0
- data/README +98 -0
- data/Rakefile +50 -0
- data/db/migrate/001_create_searchable.rb +33 -0
- data/init.rb +25 -0
- data/install.rb +1 -0
- data/lib/search_api.rb +13 -0
- data/lib/search_api/active_record_bridge.rb +385 -0
- data/lib/search_api/active_record_integration.rb +272 -0
- data/lib/search_api/bridge.rb +75 -0
- data/lib/search_api/callbacks.rb +65 -0
- data/lib/search_api/errors.rb +11 -0
- data/lib/search_api/search.rb +473 -0
- data/lib/search_api/sql_fragment.rb +98 -0
- data/lib/search_api/text_criterion.rb +132 -0
- data/searchapi.gemspec +35 -0
- data/tasks/search_api_tasks.rake +8 -0
- data/test/active_record_bridge_test.rb +488 -0
- data/test/active_record_integration_test.rb +49 -0
- data/test/bridge_test.rb +69 -0
- data/test/callbacks_test.rb +157 -0
- data/test/mock_model.rb +54 -0
- data/test/search_test.rb +340 -0
- data/uninstall.rb +1 -0
- metadata +98 -0
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
|