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 +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
|