searchlogic 2.3.16 → 2.4.0

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/README.rdoc CHANGED
@@ -113,6 +113,19 @@ Also, these conditions aren't limited to the scopes Searchlogic provides. You ca
113
113
 
114
114
  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?
115
115
 
116
+ === Polymorphic associations
117
+
118
+ 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:
119
+
120
+ Audit.belongs_to :auditable, :polymorphic => true
121
+ User.has_many :audits, :as => :auditable
122
+
123
+ Audit.auditable_user_type_username_equals("ben")
124
+
125
+ 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:
126
+
127
+ [polymorphic association name]_[association type]_type
128
+
116
129
  === Uses :joins not :include
117
130
 
118
131
  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:
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :major: 2
3
- :minor: 3
3
+ :minor: 4
4
4
  :build:
5
- :patch: 16
5
+ :patch: 0
@@ -51,7 +51,27 @@ module Searchlogic
51
51
  ::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
52
52
  end
53
53
 
54
- # See inner_joins, except this creates LEFT OUTER joins.
54
+ # A convenience methods to create a join on a polymorphic associations target.
55
+ # Ex:
56
+ #
57
+ # Audit.belong_to :auditable, :polymorphic => true
58
+ # User.has_many :audits, :as => :auditable
59
+ #
60
+ # Audit.inner_polymorphic_join(:user, :as => :auditable) # =>
61
+ # "INNER JOINER users ON users.id = audits.auditable_id AND audits.auditable_type = 'User'"
62
+ #
63
+ # This is used internally by searchlogic to handle accessing conditions on polymorphic associations.
64
+ def inner_polymorphic_join(target, options = {})
65
+ options[:on] ||= table_name
66
+ options[:on_table_name] ||= connection.quote_table_name(options[:on])
67
+ options[:target_table] ||= connection.quote_table_name(target.to_s.pluralize)
68
+ options[:as] ||= "owner"
69
+ postgres = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
70
+ "INNER JOIN #{options[:target_table]} ON #{options[:target_table]}.id = #{options[:on_table_name]}.#{options[:as]}_id AND " +
71
+ "#{options[:on_table_name]}.#{options[:as]}_type = #{postgres ? "E" : ""}'#{target.to_s.camelize}'"
72
+ end
73
+
74
+ # See inner_joins. Does the same thing except creates LEFT OUTER joins.
55
75
  def left_outer_joins(association_name)
56
76
  ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
57
77
  end
@@ -13,7 +13,7 @@ module Searchlogic
13
13
 
14
14
  def method_missing(name, *args, &block)
15
15
  if !local_condition?(name) && details = association_condition_details(name)
16
- create_association_condition(details[:association], details[:condition], args)
16
+ create_association_condition(details[:association], details[:condition], args, details[:poly_class])
17
17
  send(name, *args)
18
18
  else
19
19
  super
@@ -21,38 +21,52 @@ module Searchlogic
21
21
  end
22
22
 
23
23
  def association_condition_details(name, last_condition = nil)
24
- assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
25
- return nil if assocs.empty?
26
-
24
+ non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
25
+ poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
26
+ return nil if non_poly_assocs.empty? && poly_assocs.empty?
27
+
27
28
  name_with_condition = [name, last_condition].compact.join('_')
28
- if name_with_condition.to_s =~ /^(#{assocs.collect(&:name).join("|")})_(\w+)$/
29
+
30
+ association_name = nil
31
+ poly_type = nil
32
+ condition = nil
33
+
34
+ if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/
29
35
  association_name = $1
30
36
  condition = $2
37
+ elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+)_type_(\w+)$/
38
+ association_name = $1
39
+ poly_type = $2
40
+ condition = $3
41
+ end
42
+
43
+ if association_name && condition
31
44
  association = reflect_on_association(association_name.to_sym)
32
- klass = association.klass
45
+ klass = poly_type ? poly_type.camelcase.constantize : association.klass
33
46
  if klass.condition?(condition)
34
- {:association => $1, :condition => $2}
47
+ {:association => association, :poly_class => poly_type && klass, :condition => condition}
35
48
  else
36
49
  nil
37
50
  end
38
51
  end
39
52
  end
40
53
 
41
- def create_association_condition(association, condition, args)
42
- named_scope("#{association}_#{condition}", association_condition_options(association, condition, args))
54
+ def create_association_condition(association, condition_name, args, poly_class = nil)
55
+ name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
56
+ named_scope(name, association_condition_options(association, condition_name, args, poly_class))
43
57
  end
44
58
 
45
- def association_condition_options(association_name, association_condition, args)
46
- association = reflect_on_association(association_name.to_sym)
47
- scope = association.klass.send(association_condition, *args)
48
- scope_options = association.klass.named_scope_options(association_condition)
49
- arity = association.klass.named_scope_arity(association_condition)
59
+ def association_condition_options(association, association_condition, args, poly_class = nil)
60
+ klass = poly_class ? poly_class : association.klass
61
+ scope = klass.send(association_condition, *args)
62
+ scope_options = klass.named_scope_options(association_condition)
63
+ arity = klass.named_scope_arity(association_condition)
50
64
 
51
65
  if !arity || arity == 0
52
66
  # The underlying condition doesn't require any parameters, so let's just create a simple
53
67
  # named scope that is based on a hash.
54
68
  options = scope.scope(:find)
55
- prepare_named_scope_options(options, association)
69
+ prepare_named_scope_options(options, association, poly_class)
56
70
  options
57
71
  else
58
72
  proc_args = arity_args(arity)
@@ -60,9 +74,9 @@ module Searchlogic
60
74
 
61
75
  eval <<-"end_eval"
62
76
  searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
63
- scope = association.klass.send(association_condition, #{proc_args.join(",")})
77
+ scope = klass.send(association_condition, #{proc_args.join(",")})
64
78
  options = scope ? scope.scope(:find) : {}
65
- prepare_named_scope_options(options, association)
79
+ prepare_named_scope_options(options, association, poly_class)
66
80
  options
67
81
  }
68
82
  end_eval
@@ -88,13 +102,19 @@ module Searchlogic
88
102
  args
89
103
  end
90
104
 
91
- def prepare_named_scope_options(options, association)
105
+ def prepare_named_scope_options(options, association, poly_class = nil)
92
106
  options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
93
107
 
94
- options[:conditions] = association.klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
108
+ klass = poly_class || association.klass
109
+ # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
110
+ options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
111
+
112
+ poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
95
113
 
96
114
  if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
97
- options[:joins] = [inner_joins(association.name), options[:joins]].flatten
115
+ options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
116
+ elsif poly_class
117
+ options[:joins] = options[:joins].blank? ? poly_join : [poly_join, inner_joins(options[:joins])]
98
118
  else
99
119
  options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
100
120
  end
@@ -29,14 +29,15 @@ module Searchlogic
29
29
  end
30
30
 
31
31
  def association_ordering_condition_details(name)
32
- associations = reflect_on_all_associations.collect { |assoc| assoc.name }
33
- if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
34
- {:order_as => $1, :association => $2, :condition => $3}
32
+ associations = reflect_on_all_associations
33
+ association_names = associations.collect { |assoc| assoc.name }
34
+ if name.to_s =~ /^(ascend|descend)_by_(#{association_names.join("|")})_(\w+)$/
35
+ {:order_as => $1, :association => associations.find { |a| a.name == $2.to_sym }, :condition => $3}
35
36
  end
36
37
  end
37
38
 
38
39
  def create_association_ordering_condition(association, order_as, condition, args)
39
- named_scope("#{order_as}_by_#{association}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
40
+ named_scope("#{order_as}_by_#{association.name}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
40
41
  end
41
42
  end
42
43
  end
@@ -101,10 +101,10 @@ module Searchlogic
101
101
  end
102
102
 
103
103
  def full_association_path(part, last_condition, given_assoc)
104
- path = [given_assoc.to_sym]
105
- part.sub!(/^#{given_assoc}_/, "")
104
+ path = [given_assoc.name]
105
+ part.sub!(/^#{given_assoc.name}_/, "")
106
106
  klass = self
107
- while klass = klass.send(:reflect_on_association, given_assoc.to_sym)
107
+ while klass = klass.send(:reflect_on_association, given_assoc.name)
108
108
  klass = klass.klass
109
109
  if details = klass.send(:association_condition_details, part, last_condition)
110
110
  path << details[:association]
data/searchlogic.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{searchlogic}
8
- s.version = "2.3.16"
8
+ s.version = "2.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Ben Johnson of Binary Logic"]
12
- s.date = %q{2010-01-22}
12
+ s.date = %q{2010-01-23}
13
13
  s.description = %q{Searchlogic makes using ActiveRecord named scopes easier and less repetitive.}
14
14
  s.email = %q{bjohnson@binarylogic.com}
15
15
  s.extra_rdoc_files = [
@@ -140,6 +140,13 @@ describe "Association Conditions" do
140
140
 
141
141
  it "should sanitize the scope on a foreign model instead of passing the raw options back to the original" do
142
142
  Company.named_scope(:users_count_10, :conditions => {:users_count => 10})
143
- User.company_users_count_10.proxy_options.should == {:conditions => "\"users\".\"users_count\" = 10", :joins => :company}
143
+ User.company_users_count_10.proxy_options.should == {:conditions => "\"companies\".\"users_count\" = 10", :joins => :company}
144
+ end
145
+
146
+ it "should polymorph" do
147
+ Audit.auditable_user_type_name_like("ben").proxy_options.should == {
148
+ :conditions => ["users.name LIKE ?", "%ben%"],
149
+ :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
150
+ }
144
151
  end
145
152
  end
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,11 @@ ActiveRecord::Base.configurations = true
11
11
 
12
12
  ActiveRecord::Schema.verbose = false
13
13
  ActiveRecord::Schema.define(:version => 1) do
14
+ create_table :audits do |t|
15
+ t.string :auditable_type
16
+ t.integer :auditable_id
17
+ end
18
+
14
19
  create_table :companies do |t|
15
20
  t.datetime :created_at
16
21
  t.datetime :updated_at
@@ -66,6 +71,10 @@ require 'searchlogic'
66
71
 
67
72
  Spec::Runner.configure do |config|
68
73
  config.before(:each) do
74
+ class Audit < ActiveRecord::Base
75
+ belongs_to :auditable, :polymorphic => true
76
+ end
77
+
69
78
  class Company < ActiveRecord::Base
70
79
  has_many :users, :dependent => :destroy
71
80
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchlogic
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.16
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Johnson of Binary Logic
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-22 00:00:00 -05:00
12
+ date: 2010-01-23 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency