rd_searchlogic 3.0.0.rc
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +308 -0
- data/Rakefile +42 -0
- data/VERSION.yml +5 -0
- data/init.rb +1 -0
- data/lib/searchlogic/active_record/association_proxy.rb +19 -0
- data/lib/searchlogic/active_record/consistency.rb +49 -0
- data/lib/searchlogic/active_record/named_scope_tools.rb +102 -0
- data/lib/searchlogic/core_ext/object.rb +43 -0
- data/lib/searchlogic/core_ext/proc.rb +17 -0
- data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
- data/lib/searchlogic/named_scopes/association_conditions.rb +163 -0
- data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
- data/lib/searchlogic/named_scopes/conditions.rb +232 -0
- data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
- data/lib/searchlogic/named_scopes/ordering.rb +74 -0
- data/lib/searchlogic/rails_helpers.rb +79 -0
- data/lib/searchlogic/search.rb +259 -0
- data/lib/searchlogic.rb +89 -0
- data/rails/init.rb +1 -0
- data/spec/searchlogic/active_record/association_proxy_spec.rb +23 -0
- data/spec/searchlogic/active_record/consistency_spec.rb +28 -0
- data/spec/searchlogic/core_ext/object_spec.rb +9 -0
- data/spec/searchlogic/core_ext/proc_spec.rb +8 -0
- data/spec/searchlogic/named_scopes/alias_scope_spec.rb +23 -0
- data/spec/searchlogic/named_scopes/association_conditions_spec.rb +221 -0
- data/spec/searchlogic/named_scopes/association_ordering_spec.rb +29 -0
- data/spec/searchlogic/named_scopes/conditions_spec.rb +321 -0
- data/spec/searchlogic/named_scopes/or_conditions_spec.rb +66 -0
- data/spec/searchlogic/named_scopes/ordering_spec.rb +34 -0
- data/spec/searchlogic/search_spec.rb +459 -0
- data/spec/spec_helper.rb +146 -0
- metadata +123 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ben Johnson of Binary Logic
|
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/README.rdoc
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
= Searchlogic
|
2
|
+
|
3
|
+
Searchlogic makes using ActiveRecord named scopes easier and less repetitive. It helps keep your code DRY, clean, and simple.
|
4
|
+
|
5
|
+
== Helpful links
|
6
|
+
|
7
|
+
* <b>Documentation:</b> http://rdoc.info/projects/binarylogic/searchlogic
|
8
|
+
* <b>Repository:</b> http://github.com/binarylogic/searchlogic/tree/master
|
9
|
+
* <b>Issues:</b> http://github.com/binarylogic/searchlogic/issues
|
10
|
+
* <b>Google group:</b> http://groups.google.com/group/searchlogic
|
11
|
+
* <b>Railscast:</b> http://railscasts.com/episodes/176-searchlogic
|
12
|
+
|
13
|
+
<b>Before contacting me directly, please read:</b>
|
14
|
+
|
15
|
+
If you find a bug or a problem please post it in the issues section. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.
|
16
|
+
|
17
|
+
== Install & use
|
18
|
+
|
19
|
+
Install the gem from rubyforge:
|
20
|
+
|
21
|
+
sudo gem install searchlogic
|
22
|
+
|
23
|
+
Now just set it as a dependency in your project and you are ready to go.
|
24
|
+
|
25
|
+
You can also install this as a plugin:
|
26
|
+
|
27
|
+
script/plugin install git://github.com/binarylogic/searchlogic.git
|
28
|
+
|
29
|
+
See below for usage examples.
|
30
|
+
|
31
|
+
== Search using conditions on columns
|
32
|
+
|
33
|
+
Instead of explaining what Searchlogic can do, let me show you. Let's start at the top:
|
34
|
+
|
35
|
+
# We have the following model
|
36
|
+
User(id: integer, created_at: datetime, username: string, age: integer)
|
37
|
+
|
38
|
+
# Searchlogic gives you a bunch of named scopes for free:
|
39
|
+
User.username_equals("bjohnson")
|
40
|
+
User.username_equals(["bjohnson", "thunt"])
|
41
|
+
User.username_equals("a".."b")
|
42
|
+
User.username_does_not_equal("bjohnson")
|
43
|
+
User.username_begins_with("bjohnson")
|
44
|
+
User.username_not_begin_with("bjohnson")
|
45
|
+
User.username_like("bjohnson")
|
46
|
+
User.username_not_like("bjohnson")
|
47
|
+
User.username_ends_with("bjohnson")
|
48
|
+
User.username_not_end_with("bjohnson")
|
49
|
+
User.age_greater_than(20)
|
50
|
+
User.age_greater_than_or_equal_to(20)
|
51
|
+
User.age_less_than(20)
|
52
|
+
User.age_less_than_or_equal_to(20)
|
53
|
+
User.username_null
|
54
|
+
User.username_not_null
|
55
|
+
User.username_blank
|
56
|
+
|
57
|
+
Any named scope Searchlogic creates is dynamic and created via method_missing. Meaning it will only create what you need. Also, keep in mind, these are just named scopes, you can chain them, call methods off of them, etc:
|
58
|
+
|
59
|
+
scope = User.username_like("bjohnson").age_greater_than(20).id_less_than(55)
|
60
|
+
scope.all
|
61
|
+
scope.first
|
62
|
+
scope.count
|
63
|
+
# etc...
|
64
|
+
|
65
|
+
For a complete list of conditions please see the constants in Searchlogic::NamedScopes::Conditions.
|
66
|
+
|
67
|
+
== Use condition aliases
|
68
|
+
|
69
|
+
Typing out 'greater_than_or_equal_to' is not fun. Instead Searchlogic provides various aliases for the conditions. For a complete list please see Searchlogic::NamedScopes::Conditions. But they are pretty straightforward:
|
70
|
+
|
71
|
+
User.username_is(10) # equals
|
72
|
+
User.username_eq(10) # equals
|
73
|
+
User.id_lt(10) # less than
|
74
|
+
User.id_lte(10) # less than or equal to
|
75
|
+
User.id_gt(10) # greater than
|
76
|
+
User.id_gte(10) # greater than or equal to
|
77
|
+
# etc...
|
78
|
+
|
79
|
+
== Search using scopes in associated classes
|
80
|
+
|
81
|
+
This is my favorite part of Searchlogic. You can dynamically call scopes on associated classes and Searchlogic will take care of creating the necessary joins for you. This is REALY nice for keeping your code DRY. The best way to explain this is to show you:
|
82
|
+
|
83
|
+
=== Searchlogic provided scopes
|
84
|
+
|
85
|
+
Let's take some basic scopes that Searchlogic provides for every model:
|
86
|
+
|
87
|
+
# We have the following relationships
|
88
|
+
User.has_many :orders
|
89
|
+
Order.has_many :line_items
|
90
|
+
LineItem
|
91
|
+
|
92
|
+
# Set conditions on association columns
|
93
|
+
User.orders_total_greater_than(20)
|
94
|
+
User.orders_line_items_price_greater_than(20)
|
95
|
+
|
96
|
+
# Order by association columns
|
97
|
+
User.ascend_by_order_total
|
98
|
+
User.descend_by_orders_line_items_price
|
99
|
+
|
100
|
+
This is recursive, you can travel through your associations simply by typing it in the name of the method. Again these are just named scopes. You can chain them together, call methods off of them, etc.
|
101
|
+
|
102
|
+
=== Custom associated scopes
|
103
|
+
|
104
|
+
Also, these conditions aren't limited to the scopes Searchlogic provides. You can use your own scopes. Like this:
|
105
|
+
|
106
|
+
LineItem.named_scope :expensive, :conditions => "line_items.price > 500"
|
107
|
+
|
108
|
+
User.orders_line_items_expensive
|
109
|
+
|
110
|
+
As I stated above, Searchlogic will take care of creating the necessary joins for you. This is REALLY nice when trying to keep your code DRY, because if you wanted to use a scope like this in your User model you would have to copy over the conditions. Now you have 2 named scopes that are essentially doing the same thing. Why do that when you can dynamically access that scope using this feature?
|
111
|
+
|
112
|
+
=== Polymorphic associations
|
113
|
+
|
114
|
+
Polymorphic associations are tough because ActiveRecord doesn't support them with the :joins or :include options. Searchlogic checks for a specific syntax and takes care of this for you. Ex:
|
115
|
+
|
116
|
+
Audit.belongs_to :auditable, :polymorphic => true
|
117
|
+
User.has_many :audits, :as => :auditable
|
118
|
+
|
119
|
+
Audit.auditable_user_type_username_equals("ben")
|
120
|
+
|
121
|
+
The above will take care of creating the inner join on the polymorphic association so that it only looks for type 'User'. On the surface it works the same as a non polymorphic association. The syntax difference being that you need to call the association and then specify the type:
|
122
|
+
|
123
|
+
[polymorphic association name]_[association type]_type
|
124
|
+
|
125
|
+
=== Uses :joins not :include
|
126
|
+
|
127
|
+
Another thing to note is that the joins created by Searchlogic do NOT use the :include option, making them <em>much</em> faster. Instead they leverage the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
|
128
|
+
|
129
|
+
Benchmark.bm do |x|
|
130
|
+
x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
|
131
|
+
x.report { 10.times { Event.tickets_id_gt(10).all } }
|
132
|
+
end
|
133
|
+
user system total real
|
134
|
+
10.120000 0.170000 10.290000 ( 12.625521)
|
135
|
+
2.630000 0.050000 2.680000 ( 3.313754)
|
136
|
+
|
137
|
+
If you want to use the :include option, just specify it:
|
138
|
+
|
139
|
+
User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items})
|
140
|
+
|
141
|
+
Obviously, only do this if you want to actually use the included objects. Including objects into a query can be helpful with performance, especially when solving an N+1 query problem.
|
142
|
+
|
143
|
+
== Order your search
|
144
|
+
|
145
|
+
Just like the various conditions, Searchlogic gives you some very basic scopes for ordering your data:
|
146
|
+
|
147
|
+
User.ascend_by_id
|
148
|
+
User.descend_by_id
|
149
|
+
User.ascend_by_orders_line_items_price
|
150
|
+
# etc...
|
151
|
+
|
152
|
+
== Use any or all
|
153
|
+
|
154
|
+
Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
|
155
|
+
|
156
|
+
User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
|
157
|
+
User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
|
158
|
+
User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
|
159
|
+
|
160
|
+
This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
|
161
|
+
|
162
|
+
== Combine scopes with 'OR'
|
163
|
+
|
164
|
+
In the same fashion that Searchlogic provides a tool for accessing scopes in associated classes, it also provides a tool for combining scopes with 'OR'. As we all know, when scopes are combined they are joined with 'AND', but sometimes you need to combine scopes with 'OR'. Searchlogic solves this problem:
|
165
|
+
|
166
|
+
User.username_or_first_name_like("ben")
|
167
|
+
=> "username LIKE '%ben%' OR first_name like'%ben%'"
|
168
|
+
|
169
|
+
User.id_or_age_lt_or_username_or_first_name_begins_with(10)
|
170
|
+
=> "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
|
171
|
+
|
172
|
+
Notice you don't have to specify the explicit condition (like, gt, lt, begins with, etc.). You just need to eventually specify it. If you specify a column it will just use the next condition specified. So instead of:
|
173
|
+
|
174
|
+
User.username_like_or_first_name_like("ben")
|
175
|
+
|
176
|
+
You can do:
|
177
|
+
|
178
|
+
User.username_or_first_name_like("ben")
|
179
|
+
|
180
|
+
Again, these just map to named scopes. Use Searchlogic's dynamic scopes, use scopes on associations, use your own custom scopes. As long as it maps to a named scope it will join the conditions with 'OR'. There are no limitations.
|
181
|
+
|
182
|
+
== Create scope procedures
|
183
|
+
|
184
|
+
Sometimes you notice a pattern in your application where you are constantly combining certain named scopes. You want to keep the flexibility of being able to mix and match small named scopes, while at the same time being able to call a single scope for a common task. User searchlogic's scpe procedure:
|
185
|
+
|
186
|
+
User.scope_procedure :awesome, lambda { first_name_begins_with("ben").last_name_begins_with("johnson").website_equals("binarylogic.com") }
|
187
|
+
|
188
|
+
All that this is doing is creating a class level method, but what is nice about this method is that is more inline with your other named scopes. It also tells searchlogic that this method is 'safe' to use when using the search method. Ex:
|
189
|
+
|
190
|
+
User.search(:awesome => true)
|
191
|
+
|
192
|
+
Otherwise searchlogic will ignore the 'awesome' condition because there is no way to tell that its a valid scope. This is a security measure to keep users from passing in a scope with a named like 'destroy_all'.
|
193
|
+
|
194
|
+
== Make searching and ordering data in your application trivial
|
195
|
+
|
196
|
+
The above is great, but what about tying all of this in with a search form in your application? What would be really nice is if we could use an object that represented a single search. Like this...
|
197
|
+
|
198
|
+
search = User.search(:username_like => "bjohnson", :age_less_than => 20)
|
199
|
+
search.all
|
200
|
+
|
201
|
+
The above is equivalent to:
|
202
|
+
|
203
|
+
User.username_like("bjohnson").age_less_than(20).all
|
204
|
+
|
205
|
+
You can set, read, and chain conditions off of your search too:
|
206
|
+
|
207
|
+
search.username_like => "bjohnson"
|
208
|
+
search.age_gt = 2 => 2
|
209
|
+
search.id_gt(10).email_begins_with("bjohnson") => <#Searchlogic::Search...>
|
210
|
+
search.all => An array of users
|
211
|
+
search.count => integer
|
212
|
+
# .. etc
|
213
|
+
|
214
|
+
So let's start with the controller...
|
215
|
+
|
216
|
+
=== Your controller
|
217
|
+
|
218
|
+
The search class just chains named scopes together for you. What's so great about that? It keeps your controllers extremely simple:
|
219
|
+
|
220
|
+
class UsersController < ApplicationController
|
221
|
+
def index
|
222
|
+
@search = User.search(params[:search])
|
223
|
+
@users = @search.all
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
It doesn't get any simpler than that.
|
228
|
+
|
229
|
+
=== Your form
|
230
|
+
|
231
|
+
Adding a search condition is as simple as adding a condition to your form. Remember all of those named scopes above? Just create fields with the same names:
|
232
|
+
|
233
|
+
- form_for @search do |f|
|
234
|
+
= f.text_field :username_like
|
235
|
+
= f.select :age_greater_than, (0..100)
|
236
|
+
= f.text_field :orders_total_greater_than
|
237
|
+
= f.submit
|
238
|
+
|
239
|
+
When a Searchlogic::Search object is passed to form_for it will add a hidden field for the "order" condition, to preserve the order of the data.
|
240
|
+
|
241
|
+
=== Additional helpers
|
242
|
+
|
243
|
+
There really isn't a big need for helpers in searchlogic, other than helping you order data. If you want to order your search with a link, just specify the name of the column. Ex:
|
244
|
+
|
245
|
+
= order @search, :by => :age
|
246
|
+
= order @search, :by => :created_at, :as => "Created date"
|
247
|
+
|
248
|
+
The first one will create a link that alternates between calling "ascend_by_age" and "descend_by_age". If you wanted to order your data by more than just a column, create your own named scopes: "ascend_by_*" and "descend_by_*". The "order" helper is a very straight forward helper, checkout the docs for some of the options.
|
249
|
+
|
250
|
+
<b>This helper is just a convenience method. It's extremely simple and there is nothing wrong with creating your own. If it doesn't do what you want, copy the code, modify it, and create your own. You could even fork the project, modify it there, and use your own gem.</b>
|
251
|
+
|
252
|
+
== Use your existing named scopes
|
253
|
+
|
254
|
+
This is one of the big differences between Searchlogic v1 and v2. What about your existing named scopes? Let's say you have this:
|
255
|
+
|
256
|
+
User.named_scope :four_year_olds, :conditions => {:age => 4}
|
257
|
+
|
258
|
+
Again, these are all just named scopes, use it in the same way:
|
259
|
+
|
260
|
+
User.search(:four_year_olds => true, :username_like => "bjohnson")
|
261
|
+
|
262
|
+
Notice we pass true as the value. If a named scope does not accept any parameters (arity == 0) you can simply pass it true or false. If you pass false, the named scope will be ignored. If your named scope accepts a parameter, the value will be passed right to the named scope regardless of the value.
|
263
|
+
|
264
|
+
Now just throw it in your form:
|
265
|
+
|
266
|
+
- form_for @search do |f|
|
267
|
+
= f.text_field :username_like
|
268
|
+
= f.check_box :four_year_olds
|
269
|
+
= f.submit
|
270
|
+
|
271
|
+
This really allows Searchlogic to extend beyond what it provides internally. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope and use it. The sky is the limit.
|
272
|
+
|
273
|
+
== Pagination (leverage will_paginate)
|
274
|
+
|
275
|
+
Instead of recreating the wheel with pagination, Searchlogic works great with will_paginate. All that Searchlogic is doing is creating named scopes, and will_paginate works great with named scopes:
|
276
|
+
|
277
|
+
User.username_like("bjohnson").age_less_than(20).paginate(:page => params[:page])
|
278
|
+
User.search(:username_like => "bjohnson", :age_less_than => 20).paginate(:page => params[:page])
|
279
|
+
|
280
|
+
If you don't like will_paginate, use another solution, or roll your own. Pagination really has nothing to do with searching, and the main goal for Searchlogic v2 was to keep it lean and simple. No reason to recreate the wheel and bloat the library.
|
281
|
+
|
282
|
+
== Conflicts with other gems
|
283
|
+
|
284
|
+
You will notice searchlogic wants to create a method called "search". So do other libraries like thinking-sphinx, etc. So searchlogic has a no conflict resolution. If the "search" method is already taken the method will be called "searchlogic" instead. So instead of
|
285
|
+
|
286
|
+
User.search
|
287
|
+
|
288
|
+
You would do:
|
289
|
+
|
290
|
+
User.searchlogic
|
291
|
+
|
292
|
+
== Under the hood
|
293
|
+
|
294
|
+
Before I use a library in my application I like to glance at the source and try to at least understand the basics of how it works. If you are like me, a nice little explanation from the author is always helpful:
|
295
|
+
|
296
|
+
Searchlogic utilizes method_missing to create all of these named scopes. When it hits method_missing it creates a named scope to ensure it will never hit method missing for that named scope again. Sort of a caching mechanism. It works in the same fashion as ActiveRecord's "find_by_*" methods. This way only the named scopes you need are created and nothing more.
|
297
|
+
|
298
|
+
The search object is just a proxy to your model that only delegates calls that map to named scopes and nothing more. This is obviously done for security reasons. It also helps make form integration easier, by type casting values, and playing nice with form_for. This class is pretty simple as well.
|
299
|
+
|
300
|
+
That's about it, the named scope options are pretty bare bones and created just like you would manually.
|
301
|
+
|
302
|
+
== Credit
|
303
|
+
|
304
|
+
Thanks a lot to {Tyler Hunt}[http://github.com/tylerhunt] for helping plan, design, and start the project. He was a big help.
|
305
|
+
|
306
|
+
== Copyright
|
307
|
+
|
308
|
+
Copyright (c) 2009 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "rd_searchlogic"
|
8
|
+
gem.summary = "Searchlogic makes using ActiveRecord named scopes easier and less repetitive."
|
9
|
+
gem.description = "Searchlogic makes using ActiveRecord named scopes easier and less repetitive."
|
10
|
+
gem.email = "bjohnson@binarylogic.com"
|
11
|
+
gem.homepage = "http://github.com/railsdog/searchlogic"
|
12
|
+
gem.authors = ["Ben Johnson of Binary Logic"]
|
13
|
+
gem.rubyforge_project = "searchlogic"
|
14
|
+
gem.add_dependency "activerecord", ">= 3.0.0.beta4"
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
23
|
+
spec.libs << 'lib' << 'spec'
|
24
|
+
spec.spec_files = FileList[
|
25
|
+
#'spec/**/conditions_spec.rb',
|
26
|
+
#'spec/**/ordering_conditions_spec.rb',
|
27
|
+
'spec/**/association_conditions_spec.rb'
|
28
|
+
#'spec/**/association_ordering_spec.rb'
|
29
|
+
#'spec/**/search_spec.rb'
|
30
|
+
#'spec/**/*_spec.rb'
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
35
|
+
spec.libs << 'lib' << 'spec'
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :spec => :check_dependencies
|
41
|
+
|
42
|
+
task :default => :spec
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "searchlogic"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module ActiveRecord
|
3
|
+
module AssociationProxy
|
4
|
+
def self.included(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
alias_method_chain :send, :searchlogic
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_with_searchlogic(method, *args)
|
11
|
+
if !proxy_respond_to?(method) && !proxy_reflection.options[:polymorphic] && proxy_reflection.klass.condition?(method)
|
12
|
+
proxy_reflection.klass.send(method, *args)
|
13
|
+
else
|
14
|
+
send_without_searchlogic(method, *args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module ActiveRecord
|
3
|
+
# Active Record is pretty inconsistent with how their SQL is constructed. This
|
4
|
+
# method attempts to close the gap between the various inconsistencies.
|
5
|
+
module Consistency
|
6
|
+
def self.included(klass)
|
7
|
+
klass.class_eval do
|
8
|
+
alias_method_chain :merge_joins, :singularity
|
9
|
+
alias_method_chain :merge_joins, :consistent_conditions
|
10
|
+
alias_method_chain :merge_joins, :merged_duplicates
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# In AR multiple joins are sometimes in a single join query, and other times they
|
15
|
+
# are not. The merge_joins method in AR should account for this, but it doesn't.
|
16
|
+
# This fixes that problem. This way there is one join per string, which allows
|
17
|
+
# the merge_joins method to delete duplicates.
|
18
|
+
def merge_joins_with_singularity(*args)
|
19
|
+
joins = merge_joins_without_singularity(*args)
|
20
|
+
joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method ensures that the order of the conditions in the joins are the same.
|
24
|
+
# The strings of the joins MUST be exactly the same for AR to remove the duplicates.
|
25
|
+
# AR is not consistent in this approach, resulting in duplicate joins errors when
|
26
|
+
# combining scopes.
|
27
|
+
def merge_joins_with_consistent_conditions(*args)
|
28
|
+
joins = merge_joins_without_consistent_conditions(*args)
|
29
|
+
joins.collect do |j|
|
30
|
+
if j.is_a?(String) && (j =~ / (AND|OR) /i).nil?
|
31
|
+
j.gsub(/(.*) ON (.*) = (.*)/) do |m|
|
32
|
+
join, cond1, cond2 = $1, $2, $3
|
33
|
+
sorted = [cond1.gsub(/\(|\)/, ""), cond2.gsub(/\(|\)/, "")].sort
|
34
|
+
"#{join} ON #{sorted[0]} = #{sorted[1]}"
|
35
|
+
end
|
36
|
+
else
|
37
|
+
j
|
38
|
+
end
|
39
|
+
end.uniq
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def merge_joins_with_merged_duplicates(*args)
|
44
|
+
args << "" if !Thread.current["searchlogic_delegation"]
|
45
|
+
joins = merge_joins_without_merged_duplicates(*args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module ActiveRecord
|
3
|
+
# Adds methods that give extra information about a classes named scopes.
|
4
|
+
module NamedScopeTools
|
5
|
+
# Retrieves the options passed when creating the respective named scope. Ex:
|
6
|
+
#
|
7
|
+
# named_scope :whatever, :conditions => {:column => value}
|
8
|
+
#
|
9
|
+
# This method will return:
|
10
|
+
#
|
11
|
+
# :conditions => {:column => value}
|
12
|
+
#
|
13
|
+
# ActiveRecord hides this internally in a Proc, so we have to try and pull it out with this
|
14
|
+
# method.
|
15
|
+
def named_scope_options(name)
|
16
|
+
key = scopes.key?(name.to_sym) ? name.to_sym : condition_scope_name(name)
|
17
|
+
|
18
|
+
if key
|
19
|
+
#TODO: recover the find options from relation
|
20
|
+
eval("options", scopes[key].binding)
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The arity for a named scope's proc is important, because we use the arity
|
27
|
+
# to determine if the condition should be ignored when calling the search method.
|
28
|
+
# If the condition is false and the arity is 0, then we skip it all together. Ex:
|
29
|
+
#
|
30
|
+
# User.named_scope :age_is_4, :conditions => {:age => 4}
|
31
|
+
# User.search(:age_is_4 => false) == User.all
|
32
|
+
# User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
|
33
|
+
#
|
34
|
+
# We also use it when trying to "copy" the underlying named scope for association
|
35
|
+
# conditions. This way our aliased scope accepts the same number of parameters for
|
36
|
+
# the underlying scope.
|
37
|
+
def named_scope_arity(name)
|
38
|
+
options = named_scope_options(name)
|
39
|
+
options.respond_to?(:arity) ? options.arity : nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# When searchlogic calls a named_scope on a foreigh model it will execute that scope and then call scope(:find).
|
43
|
+
# When we get these options we want this to be in an exclusive scope, especially if we are calling a condition on
|
44
|
+
# the same originating model:
|
45
|
+
#
|
46
|
+
# Company.users_company_name_equals("name")
|
47
|
+
#
|
48
|
+
# If we aren't in an exclusive scope we will get unexpected results for the :joins option. Lastly, we want the named_scopes
|
49
|
+
# generated by searchlogic to be symbols whenever possible. The reason for this is so that we can allow
|
50
|
+
# ActiveRecord to leverage its joins library that automatically aliases joins if they appear more than once in a query.
|
51
|
+
# If the joins are strings, AtiveRecord can't do anything. Because the code that does this in ActiveRecord is pretty bad
|
52
|
+
# when it comes to being consisitent, searchlogic had to fix this in Searchloigc::ActiveRecord::Consistency. That said,
|
53
|
+
# part of this fix is to normalize joins into strings. We do not want to do this if we are calling scopes on foreigh models.
|
54
|
+
# Only when we are performing an action on it. This is what the searchlogic_delegation thread variable is all about. A
|
55
|
+
# flag to let search logic know not to convert joins to strings.
|
56
|
+
def in_searchlogic_delegation(&block)
|
57
|
+
old = Thread.current["searchlogic_delegation"]
|
58
|
+
Thread.current["searchlogic_delegation"] = true
|
59
|
+
with_exclusive_scope(&block)
|
60
|
+
Thread.current["searchlogic_delegation"] = old
|
61
|
+
end
|
62
|
+
|
63
|
+
# A convenience method for creating inner join sql to that your inner joins
|
64
|
+
# are consistent with how Active Record creates them. Basically a tool for
|
65
|
+
# you to use when writing your own named scopes. This way you know for sure
|
66
|
+
# that duplicate joins will be removed when chaining scopes together that
|
67
|
+
# use the same join.
|
68
|
+
#
|
69
|
+
# Also, don't worry about breaking up the joins or retriving multiple joins.
|
70
|
+
# ActiveRecord will remove dupilicate joins and Searchlogic assists ActiveRecord in
|
71
|
+
# breaking up your joins so that they are unique.
|
72
|
+
def inner_joins(association_name)
|
73
|
+
::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
|
74
|
+
end
|
75
|
+
|
76
|
+
# A convenience methods to create a join on a polymorphic associations target.
|
77
|
+
# Ex:
|
78
|
+
#
|
79
|
+
# Audit.belong_to :auditable, :polymorphic => true
|
80
|
+
# User.has_many :audits, :as => :auditable
|
81
|
+
#
|
82
|
+
# Audit.inner_polymorphic_join(:user, :as => :auditable) # =>
|
83
|
+
# "INNER JOINER users ON users.id = audits.auditable_id AND audits.auditable_type = 'User'"
|
84
|
+
#
|
85
|
+
# This is used internally by searchlogic to handle accessing conditions on polymorphic associations.
|
86
|
+
def inner_polymorphic_join(target, options = {})
|
87
|
+
options[:on] ||= table_name
|
88
|
+
options[:on_table_name] ||= connection.quote_table_name(options[:on])
|
89
|
+
options[:target_table] ||= connection.quote_table_name(target.to_s.pluralize)
|
90
|
+
options[:as] ||= "owner"
|
91
|
+
postgres = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
92
|
+
"INNER JOIN #{options[:target_table]} ON #{options[:target_table]}.id = #{options[:on_table_name]}.#{options[:as]}_id AND " +
|
93
|
+
"#{options[:on_table_name]}.#{options[:as]}_type = #{postgres ? "E" : ""}'#{target.to_s.camelize}'"
|
94
|
+
end
|
95
|
+
|
96
|
+
# See inner_joins. Does the same thing except creates LEFT OUTER joins.
|
97
|
+
def left_outer_joins(association_name)
|
98
|
+
::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module CoreExt
|
3
|
+
# Contains extensions for the Object class that Searchlogic uses.
|
4
|
+
module Object
|
5
|
+
# Searchlogic needs to know the expected type of the condition value so that it can properly cast
|
6
|
+
# the value in the Searchlogic::Search object. For example:
|
7
|
+
#
|
8
|
+
# search = User.search(:id_gt => "1")
|
9
|
+
#
|
10
|
+
# You would expect this:
|
11
|
+
#
|
12
|
+
# search.id_gt => 1
|
13
|
+
#
|
14
|
+
# Not this:
|
15
|
+
#
|
16
|
+
# search.id_gt => "1"
|
17
|
+
#
|
18
|
+
# Parameter values from forms are ALWAYS strings, so we have to cast them. Just like ActiveRecord
|
19
|
+
# does when you instantiate a new User object.
|
20
|
+
#
|
21
|
+
# The problem is that ruby has no variable types, so Searchlogic needs to know what type you are expecting
|
22
|
+
# for your named scope. So instead of this:
|
23
|
+
#
|
24
|
+
# named_scope :id_gt, lambda { |value| {:conditions => ["id > ?", value]} }
|
25
|
+
#
|
26
|
+
# You need to do this:
|
27
|
+
#
|
28
|
+
# named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
|
29
|
+
#
|
30
|
+
# If you are wanting a string, you don't have to do anything, because Searchlogic assumes you want a string.
|
31
|
+
# If you want something else, you need to specify it as I did in the above example. Comments are appreciated
|
32
|
+
# on this, if you know of a better solution please let me know. But this is the best I could come up with,
|
33
|
+
# without being intrusive and altering default behavior.
|
34
|
+
def searchlogic_lambda(type = :string, options = {}, &block)
|
35
|
+
proc = lambda(&block)
|
36
|
+
proc.searchlogic_options ||= {}
|
37
|
+
proc.searchlogic_options[:type] = type
|
38
|
+
proc.searchlogic_options.merge!(options)
|
39
|
+
proc
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module CoreExt
|
3
|
+
module Proc # :nodoc:
|
4
|
+
def self.included(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
attr_accessor :searchlogic_options
|
7
|
+
|
8
|
+
def searchlogic_options
|
9
|
+
@searchlogic_options ||= {}
|
10
|
+
@searchlogic_options[:type] ||= :string
|
11
|
+
@searchlogic_options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|