sayso-meta_where 1.0.4.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG +90 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +346 -0
  7. data/Rakefile +11 -0
  8. data/lib/core_ext/hash.rb +5 -0
  9. data/lib/core_ext/symbol.rb +39 -0
  10. data/lib/core_ext/symbol_operators.rb +48 -0
  11. data/lib/meta_where/association_reflection.rb +51 -0
  12. data/lib/meta_where/column.rb +31 -0
  13. data/lib/meta_where/compound.rb +20 -0
  14. data/lib/meta_where/condition.rb +32 -0
  15. data/lib/meta_where/condition_operators.rb +19 -0
  16. data/lib/meta_where/function.rb +108 -0
  17. data/lib/meta_where/join_dependency.rb +101 -0
  18. data/lib/meta_where/join_type.rb +43 -0
  19. data/lib/meta_where/not.rb +13 -0
  20. data/lib/meta_where/relation.rb +296 -0
  21. data/lib/meta_where/utility.rb +51 -0
  22. data/lib/meta_where/version.rb +3 -0
  23. data/lib/meta_where/visitors/attribute.rb +58 -0
  24. data/lib/meta_where/visitors/predicate.rb +149 -0
  25. data/lib/meta_where/visitors/visitor.rb +47 -0
  26. data/lib/meta_where.rb +51 -0
  27. data/meta_where.gemspec +48 -0
  28. data/test/fixtures/companies.yml +17 -0
  29. data/test/fixtures/company.rb +7 -0
  30. data/test/fixtures/data_type.rb +3 -0
  31. data/test/fixtures/data_types.yml +15 -0
  32. data/test/fixtures/developer.rb +7 -0
  33. data/test/fixtures/developers.yml +55 -0
  34. data/test/fixtures/developers_projects.yml +25 -0
  35. data/test/fixtures/fixed_bid_project.rb +2 -0
  36. data/test/fixtures/invalid_company.rb +4 -0
  37. data/test/fixtures/invalid_developer.rb +4 -0
  38. data/test/fixtures/note.rb +3 -0
  39. data/test/fixtures/notes.yml +95 -0
  40. data/test/fixtures/people.yml +14 -0
  41. data/test/fixtures/person.rb +4 -0
  42. data/test/fixtures/project.rb +7 -0
  43. data/test/fixtures/projects.yml +29 -0
  44. data/test/fixtures/schema.rb +53 -0
  45. data/test/fixtures/time_and_materials_project.rb +2 -0
  46. data/test/helper.rb +33 -0
  47. data/test/test_base.rb +21 -0
  48. data/test/test_relations.rb +476 -0
  49. metadata +184 -0
@@ -0,0 +1,31 @@
1
+ module MetaWhere
2
+ class Column
3
+ attr_reader :column, :method
4
+
5
+ def initialize(column, method)
6
+ @column = column
7
+ @method = method
8
+ end
9
+
10
+ def %(value)
11
+ MetaWhere::Condition.new(column, value, method)
12
+ end
13
+
14
+ def ==(other_column)
15
+ other_column.is_a?(Column) &&
16
+ other_column.column == column &&
17
+ other_column.method == method
18
+ end
19
+
20
+ alias_method :eql?, :==
21
+
22
+ def hash
23
+ [column, method].hash
24
+ end
25
+
26
+ # Play nicely with expand_hash_conditions_for_aggregates
27
+ def to_sym
28
+ "#{column}.#{method}".to_sym
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ require 'meta_where/condition_operators'
2
+
3
+ module MetaWhere
4
+ class Compound
5
+ include ConditionOperators
6
+
7
+ attr_reader :condition1, :condition2
8
+
9
+ def initialize(condition1, condition2)
10
+ @condition1 = condition1
11
+ @condition2 = condition2
12
+ end
13
+ end
14
+
15
+ class Or < Compound
16
+ end
17
+
18
+ class And < Compound
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ require 'meta_where/utility'
2
+ require 'meta_where/condition_operators'
3
+
4
+ module MetaWhere
5
+ class Condition
6
+ include ConditionOperators
7
+ include Utility
8
+
9
+ attr_reader :column, :value, :method
10
+
11
+ def initialize(column, value, method)
12
+ @column = column
13
+ @value = value
14
+ @method = (MetaWhere::METHOD_ALIASES[method.to_s] || method).to_s
15
+ end
16
+
17
+ def ==(other_condition)
18
+ other_condition.is_a?(Condition) &&
19
+ other_condition.column == column &&
20
+ other_condition.value == value &&
21
+ other_condition.method == method
22
+ end
23
+
24
+ alias_method :eql?, :==
25
+
26
+ # Play "nicely" with expand_hash_conditions_for_aggregates
27
+ def to_sym
28
+ self
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,19 @@
1
+ module MetaWhere
2
+ module ConditionOperators
3
+ def |(other)
4
+ Or.new(self, other)
5
+ end
6
+
7
+ def &(other)
8
+ And.new(self, other)
9
+ end
10
+
11
+ def -(other)
12
+ And.new(self, Not.new(other))
13
+ end
14
+
15
+ def -@
16
+ Not.new(self)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,108 @@
1
+ module MetaWhere
2
+ class Function
3
+ attr_reader :name, :args
4
+ attr_accessor :table, :alias
5
+
6
+ def initialize(name, *args)
7
+ @name = name
8
+ @args = args
9
+ end
10
+
11
+ def as(val)
12
+ self.alias = val
13
+ self
14
+ end
15
+
16
+ def to_sqlliteral
17
+ Arel.sql(
18
+ ("#{name}(#{(['%s'] * args.size).join(',')})" % contextualize_args) +
19
+ (self.alias ? " AS #{self.alias}" : '')
20
+ )
21
+ end
22
+
23
+ alias_method :to_sql, :to_sqlliteral
24
+
25
+ MetaWhere::PREDICATES.each do |predication|
26
+ define_method(predication) do
27
+ MetaWhere::Column.new(self, predication)
28
+ end
29
+ end
30
+
31
+ MetaWhere::METHOD_ALIASES.each_pair do |aliased, predication|
32
+ define_method(aliased) do
33
+ MetaWhere::Column.new(self, predication)
34
+ end
35
+ end
36
+
37
+ def >>(value)
38
+ MetaWhere::Condition.new(self, value, :eq)
39
+ end
40
+
41
+ def ^(value)
42
+ MetaWhere::Condition.new(self, value, :not_eq)
43
+ end
44
+
45
+ def +(value)
46
+ MetaWhere::Condition.new(self, value, :in)
47
+ end
48
+
49
+ def -(value)
50
+ MetaWhere::Condition.new(self, value, :not_in)
51
+ end
52
+
53
+ def =~(value)
54
+ MetaWhere::Condition.new(self, value, :matches)
55
+ end
56
+
57
+ # Won't work on Ruby 1.8.x so need to do this conditionally
58
+ if respond_to?('!~')
59
+ define_method('!~') do |value|
60
+ MetaWhere::Condition.new(self, value, :does_not_match)
61
+ end
62
+ end
63
+
64
+ def >(value)
65
+ MetaWhere::Condition.new(self, value, :gt)
66
+ end
67
+
68
+ def >=(value)
69
+ MetaWhere::Condition.new(self, value, :gteq)
70
+ end
71
+
72
+ def <(value)
73
+ MetaWhere::Condition.new(self, value, :lt)
74
+ end
75
+
76
+ def <=(value)
77
+ MetaWhere::Condition.new(self, value, :lteq)
78
+ end
79
+
80
+ # Play "nicely" with expand_hash_conditions_for_aggregates
81
+ def to_sym
82
+ self
83
+ end
84
+
85
+ private
86
+
87
+ def contextualize_args
88
+ args.map do |arg|
89
+ if arg.is_a? Symbol
90
+ self.table && self.table[arg] ? Arel::Visitors.for(ActiveRecord::Base).accept(table[arg]) : arg
91
+ else
92
+ arg.table = self.table if arg.is_a? MetaWhere::Function
93
+ Arel::Visitors.for(ActiveRecord::Base).accept(arg)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ module Arel
101
+ module Visitors
102
+ class ToSql
103
+ def visit_MetaWhere_Function o
104
+ o.to_sqlliteral
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,101 @@
1
+ module MetaWhere
2
+ module JoinDependency
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ alias_method_chain :build, :metawhere
7
+ alias_method_chain :find_join_association, :metawhere
8
+ end
9
+ end
10
+
11
+ class BaseMismatchError < StandardError; end
12
+ class ConfigurationError < StandardError; end
13
+ class AssociationNotFoundError < StandardError; end
14
+
15
+ def build_with_metawhere(associations, parent = nil, join_type = Arel::Nodes::InnerJoin)
16
+ parent ||= @joins.last
17
+ if MetaWhere::JoinType === associations
18
+ klass = associations.klass
19
+ join_type = associations.join_type
20
+ associations = associations.name
21
+ end
22
+
23
+ case associations
24
+ when Symbol, String
25
+ reflection = parent.reflections[associations.to_s.intern] or
26
+ raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
27
+ unless (association = find_join_association(reflection, parent)) && (!klass || association.active_record == klass)
28
+ @reflections << reflection
29
+ if reflection.options[:polymorphic]
30
+ raise ArgumentError, "You can't create a polymorphic belongs_to join without specifying the polymorphic class!" unless klass
31
+ association = PolymorphicJoinAssociation.new(reflection, self, klass, parent)
32
+ else
33
+ association = build_join_association(reflection, parent)
34
+ end
35
+ association.join_type = join_type
36
+ @joins << association
37
+ cache_joined_association(association)
38
+ end
39
+ association
40
+ else
41
+ build_without_metawhere(associations, parent, join_type)
42
+ end
43
+ end
44
+
45
+ def find_join_association_with_metawhere(name_or_reflection, parent)
46
+ case name_or_reflection
47
+ when MetaWhere::JoinType
48
+ join_associations.detect do |j|
49
+ (j.reflection.name == name_or_reflection.name) &&
50
+ (j.reflection.klass == name_or_reflection.klass) &&
51
+ (j.parent == parent)
52
+ end
53
+ else
54
+ find_join_association_without_metawhere(name_or_reflection, parent)
55
+ end
56
+ end
57
+ end
58
+
59
+ class PolymorphicJoinAssociation < ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
60
+
61
+ def initialize(reflection, join_dependency, polymorphic_class, parent = nil)
62
+ reflection.check_validity!
63
+ @active_record = polymorphic_class
64
+ @cached_record = {}
65
+ @join_dependency = join_dependency
66
+ @parent = parent || join_dependency.join_base
67
+ @reflection = reflection.clone
68
+ @reflection.instance_variable_set :"@klass", polymorphic_class
69
+ @aliased_prefix = "t#{ join_dependency.joins.size }"
70
+ @parent_table_name = @parent.active_record.table_name
71
+ @aliased_table_name = aliased_table_name_for(table_name)
72
+ @join = nil
73
+ @join_type = Arel::Nodes::InnerJoin
74
+ end
75
+
76
+ def ==(other)
77
+ other.class == self.class &&
78
+ other.reflection == reflection &&
79
+ other.active_record == active_record &&
80
+ other.parent == parent
81
+ end
82
+
83
+ def association_join
84
+ return @join if @join
85
+
86
+ aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine)
87
+ parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => arel_engine)
88
+
89
+ @join = [
90
+ aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name]),
91
+ parent_table[options[:foreign_type]].eq(active_record.name)
92
+ ]
93
+
94
+ if options[:conditions]
95
+ @join << process_conditions(options[:conditions], aliased_table_name)
96
+ end
97
+
98
+ @join
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ module MetaWhere
2
+ class JoinType
3
+ attr_reader :name, :join_type, :klass
4
+
5
+ def initialize(name, join_type = Arel::Nodes::InnerJoin, klass = nil)
6
+ @name = name
7
+ @join_type = join_type
8
+ @klass = klass
9
+ end
10
+
11
+ def ==(other)
12
+ self.class == other.class &&
13
+ name == other.name &&
14
+ join_type == other.join_type &&
15
+ klass == other.klass
16
+ end
17
+
18
+ alias_method :eql?, :==
19
+
20
+ def hash
21
+ [name, join_type, klass].hash
22
+ end
23
+
24
+ def outer
25
+ @join_type = Arel::Nodes::OuterJoin
26
+ self
27
+ end
28
+
29
+ def inner
30
+ @join_type = Arel::Nodes::InnerJoin
31
+ self
32
+ end
33
+
34
+ def type(klass)
35
+ @klass = klass
36
+ self
37
+ end
38
+
39
+ def to_sym
40
+ self
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ require 'meta_where/condition_operators'
2
+
3
+ module MetaWhere
4
+ class Not
5
+ include ConditionOperators
6
+
7
+ attr_reader :expr
8
+
9
+ def initialize(expr)
10
+ @expr = expr
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,296 @@
1
+ module MetaWhere
2
+ module Relation
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ alias_method_chain :reset, :metawhere
7
+ alias_method_chain :where_values_hash, :metawhere
8
+ end
9
+
10
+ # We have to do this on the singleton to work with Ruby 1.8.7. Not sure why.
11
+ base.instance_eval do
12
+ alias_method :&, :merge
13
+ end
14
+ end
15
+
16
+ def merge(r, association_name = nil)
17
+ if (r && (association_name || base_class.name != r.klass.base_class.name)) # Merging relations with different base.
18
+ association_name ||= (default_association = reflect_on_all_associations.detect {|a| a.class_name == r.klass.name}) ?
19
+ default_association.name : r.table_name.to_sym
20
+ r = r.clone
21
+ r.where_values.map! {|w| MetaWhere::Visitors::Predicate.visitables.include?(w.class) ? {association_name => w} : w}
22
+ r.joins_values.map! {|j| [Symbol, Hash, MetaWhere::JoinType].include?(j.class) ? {association_name => j} : j}
23
+ self.joins_values += [association_name] if reflect_on_association(association_name)
24
+ end
25
+
26
+ super(r)
27
+ end
28
+
29
+ def reset_with_metawhere
30
+ @mw_unique_joins = @mw_association_joins = @mw_non_association_joins =
31
+ @mw_stashed_association_joins = @mw_custom_joins = nil
32
+ reset_without_metawhere
33
+ end
34
+
35
+ def where_values_hash_with_metawhere
36
+ Hash[flatten_nodes(flatten_predicates(@where_values, predicate_visitor)).find_all { |w|
37
+ w.respond_to?(:operator) && w.operator == :== && w.left.relation.name == table_name
38
+ }.map { |where|
39
+ [
40
+ where.left.name,
41
+ where.right.respond_to?(:value) ? where.right.value : where.right
42
+ ]
43
+ }]
44
+ end
45
+
46
+ def build_where(opts, other = [])
47
+ if opts.is_a?(String)
48
+ [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
49
+ else
50
+ predicates = []
51
+ [opts, *other].each do |arg|
52
+ predicates += Array.wrap(
53
+ case arg
54
+ when Array
55
+ @klass.send(:sanitize_sql, arg)
56
+ when Hash
57
+ @klass.send(:expand_hash_conditions_for_aggregates, arg)
58
+ else
59
+ arg
60
+ end
61
+ )
62
+ end
63
+ predicates
64
+ end
65
+ end
66
+
67
+ def build_custom_joins(joins = [], arel = nil)
68
+ arel ||= table
69
+ joins.each do |join|
70
+ next if join.blank?
71
+
72
+ @implicit_readonly = true
73
+
74
+ case join
75
+ when ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
76
+ arel = arel.join(join.relation, Arel::Nodes::OuterJoin).on(*join.on)
77
+ when Hash, Array, Symbol
78
+ if array_of_strings?(join)
79
+ join_string = join.join(' ')
80
+ arel = arel.join(join_string)
81
+ end
82
+ else
83
+ arel = arel.join(join)
84
+ end
85
+ end
86
+
87
+ arel
88
+ end
89
+
90
+ def custom_join_sql(*joins)
91
+ arel = table
92
+ joins.each do |join|
93
+ next if join.blank?
94
+
95
+ @implicit_readonly = true
96
+
97
+ case join
98
+ when Hash, Array, Symbol
99
+ if array_of_strings?(join)
100
+ join_string = join.join(' ')
101
+ arel = arel.join(join_string)
102
+ end
103
+ else
104
+ arel = arel.join(join)
105
+ end
106
+ end
107
+ arel.joins(arel)
108
+ end unless defined?(:custom_join_sql)
109
+
110
+ def predicates_without_conflicting_equality
111
+ remove_conflicting_equality_predicates(flatten_predicates(@where_values, predicate_visitor))
112
+ end
113
+
114
+ # Very occasionally, we need to get a visitor for another relation, so it makes sense to factor
115
+ # these out into a public method despite only being two lines long.
116
+ def predicate_visitor
117
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
118
+ MetaWhere::Visitors::Predicate.new(join_dependency)
119
+ end
120
+
121
+ def attribute_visitor
122
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
123
+ MetaWhere::Visitors::Attribute.new(join_dependency)
124
+ end
125
+
126
+ # Simulate the logic that occurs in ActiveRecord::Relation.to_a
127
+ #
128
+ # @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
129
+ #
130
+ # This will let us get a dump of the SQL that will be run against the DB for debug
131
+ # purposes without actually running the query.
132
+ def debug_sql
133
+ if eager_loading?
134
+ including = (@eager_load_values + @includes_values).uniq
135
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
136
+ construct_relation_for_association_find(join_dependency).to_sql
137
+ else
138
+ arel.to_sql
139
+ end
140
+ end
141
+
142
+ def construct_limited_ids_condition(relation)
143
+ visitor = relation.attribute_visitor
144
+
145
+ relation.order_values.map! {|o| visitor.can_accept?(o) ? visitor.accept(o).to_sql : o}
146
+
147
+ super
148
+ end
149
+
150
+ def build_arel
151
+ arel = table
152
+
153
+ visitor = predicate_visitor
154
+
155
+ arel = build_intelligent_joins(arel, visitor) if @joins_values.present?
156
+
157
+ predicate_wheres = flatten_predicates(@where_values.uniq, visitor)
158
+
159
+ arel = collapse_wheres(arel, (predicate_wheres - ['']).uniq)
160
+
161
+ arel = arel.having(*flatten_predicates(@having_values, visitor).reject {|h| h.blank?}) unless @having_values.empty?
162
+
163
+ arel = arel.take(@limit_value) if @limit_value
164
+ arel = arel.skip(@offset_value) if @offset_value
165
+
166
+ arel = arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
167
+
168
+ arel = build_order(arel, attribute_visitor, @order_values) unless @order_values.empty?
169
+
170
+ arel = build_select(arel, @select_values.uniq)
171
+
172
+ arel = arel.from(@from_value) if @from_value
173
+ arel = arel.lock(@lock_value) if @lock_value
174
+
175
+ arel
176
+ end
177
+
178
+ def select(value = Proc.new)
179
+ if MetaWhere::Function === value
180
+ value.table = self.arel_table
181
+ end
182
+
183
+ super
184
+ end
185
+
186
+ private
187
+
188
+ def is_equality_predicate?(predicate)
189
+ predicate.class == Arel::Nodes::Equality
190
+ end
191
+
192
+ def build_intelligent_joins(arel, visitor)
193
+ joined_associations = []
194
+
195
+ visitor.join_dependency.graft(*stashed_association_joins)
196
+
197
+ @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
198
+
199
+ to_join = []
200
+
201
+ visitor.join_dependency.join_associations.each do |association|
202
+ if (association_relation = association.relation).is_a?(Array)
203
+ to_join << [association_relation.first, association.join_type, association.association_join.first]
204
+ to_join << [association_relation.last, association.join_type, association.association_join.last]
205
+ else
206
+ to_join << [association_relation, association.join_type, association.association_join]
207
+ end
208
+ end
209
+
210
+ to_join.each do |tj|
211
+ unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] }
212
+ joined_associations << tj
213
+ arel = arel.join(tj[0], tj[1]).on(*tj[2])
214
+ end
215
+ end
216
+
217
+ arel = arel.join(custom_joins)
218
+ end
219
+
220
+ def build_order(arel, visitor, orders)
221
+ order_attributes = orders.map {|o|
222
+ visitor.can_accept?(o) ? visitor.accept(o, visitor.join_dependency.join_base) : o
223
+ }.flatten.uniq.reject {|o| o.blank?}
224
+ order_attributes.present? ? arel.order(*order_attributes) : arel
225
+ end
226
+
227
+ def remove_conflicting_equality_predicates(predicates)
228
+ predicates.reverse.inject([]) { |ary, w|
229
+ unless is_equality_predicate?(w) && ary.any? {|p| is_equality_predicate?(p) && p.operand1.name == w.operand1.name}
230
+ ary << w
231
+ end
232
+ ary
233
+ }.reverse
234
+ end
235
+
236
+ def collapse_wheres(arel, wheres)
237
+ binaries = wheres.grep(Arel::Nodes::Binary)
238
+
239
+ groups = binaries.group_by {|b| [b.class, b.left]}
240
+
241
+ groups.each do |_, bins|
242
+ test = bins.inject(bins.shift) do |memo, expr|
243
+ memo.and(expr)
244
+ end
245
+ arel = arel.where(test)
246
+ end
247
+
248
+ (wheres - binaries).each do |where|
249
+ where = Arel.sql(where) if String === where
250
+ arel = arel.where(Arel::Nodes::Grouping.new(where))
251
+ end
252
+ arel
253
+ end
254
+
255
+ def flatten_predicates(predicates, visitor)
256
+ predicates.map {|p|
257
+ visitor.can_accept?(p) ? visitor.accept(p) : p
258
+ }.flatten.uniq
259
+ end
260
+
261
+ def flatten_nodes(nodes)
262
+ nodes.map do |n|
263
+ case n
264
+ when Arel::Nodes::Grouping
265
+ flatten_nodes([n.expr])
266
+ when Arel::Nodes::And
267
+ flatten_nodes(n.respond_to?(:children) ? n.children : [n.left, n.right])
268
+ else
269
+ n
270
+ end
271
+ end.flatten
272
+ end
273
+
274
+ def unique_joins
275
+ @mw_unique_joins ||= @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
276
+ end
277
+
278
+ def association_joins
279
+ @mw_association_joins ||= unique_joins.select{|j|
280
+ [Hash, Array, Symbol, MetaWhere::JoinType].include?(j.class) && !array_of_strings?(j)
281
+ }
282
+ end
283
+
284
+ def stashed_association_joins
285
+ @mw_stashed_association_joins ||= unique_joins.grep(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
286
+ end
287
+
288
+ def non_association_joins
289
+ @mw_non_association_joins ||= (unique_joins - association_joins - stashed_association_joins).reject {|j| j.blank?}
290
+ end
291
+
292
+ def custom_joins
293
+ @mw_custom_joins ||= custom_join_sql(*non_association_joins)
294
+ end
295
+ end
296
+ end