activerecord_where_assoc 0.1.3 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,23 +4,34 @@ require_relative "active_record_where_assoc/version"
4
4
  require "active_record"
5
5
 
6
6
  module ActiveRecordWhereAssoc
7
- # Default options for the gem. Meant to be modified in place by external code
7
+ # Default options for the gem. Meant to be modified in place by external code, such as in
8
+ # an initializer.
9
+ # Ex:
10
+ # ActiveRecordWhereAssoc.default_options[:ignore_limit] = true
11
+ #
12
+ # A description for each can be found in RelationReturningMethods@Options.
13
+ #
14
+ # :ignore_limit is the only one to consider changing, when you are using MySQL, since limit are
15
+ # never supported on it. Otherwise, the safety of having to pass the options yourself
16
+ # and noticing you made a mistake / avoiding the need for extra queries is worth the extra code.
8
17
  def self.default_options
9
18
  @default_options ||= {
10
19
  ignore_limit: false,
11
20
  never_alias_limit: false,
21
+ poly_belongs_to: :raise,
12
22
  }
13
23
  end
14
24
  end
15
25
 
16
26
  require_relative "active_record_where_assoc/core_logic"
17
- require_relative "active_record_where_assoc/query_methods"
18
- require_relative "active_record_where_assoc/querying"
27
+ require_relative "active_record_where_assoc/relation_returning_methods"
28
+ require_relative "active_record_where_assoc/relation_returning_delegates"
29
+ require_relative "active_record_where_assoc/sql_returning_methods"
19
30
 
20
31
  ActiveSupport.on_load(:active_record) do
21
32
  ActiveRecord.eager_load!
22
33
 
23
- # Need to use #send for the include to support Ruby 2.0
24
- ActiveRecord::Relation.send(:include, ActiveRecordWhereAssoc::QueryMethods)
25
- ActiveRecord::Base.extend(ActiveRecordWhereAssoc::Querying)
34
+ ActiveRecord::Relation.include(ActiveRecordWhereAssoc::RelationReturningMethods)
35
+ ActiveRecord::Base.extend(ActiveRecordWhereAssoc::RelationReturningDelegates)
36
+ ActiveRecord::Base.extend(ActiveRecordWhereAssoc::SqlReturningMethods)
26
37
  end
@@ -2,23 +2,35 @@
2
2
 
3
3
  module ActiveRecordWhereAssoc
4
4
  module ActiveRecordCompat
5
- if ActiveRecord.gem_version >= Gem::Version.new("5.1")
6
- def self.join_keys(reflection)
7
- reflection.join_keys
5
+ if ActiveRecord.gem_version >= Gem::Version.new("6.1.0.rc1")
6
+ JoinKeys = Struct.new(:key, :foreign_key)
7
+ def self.join_keys(reflection, poly_belongs_to_klass)
8
+ if poly_belongs_to_klass
9
+ JoinKeys.new(reflection.join_primary_key(poly_belongs_to_klass), reflection.join_foreign_key)
10
+ else
11
+ JoinKeys.new(reflection.join_primary_key, reflection.join_foreign_key)
12
+ end
13
+ end
14
+
15
+ elsif ActiveRecord.gem_version >= Gem::Version.new("5.1")
16
+ def self.join_keys(reflection, poly_belongs_to_klass)
17
+ if poly_belongs_to_klass
18
+ reflection.get_join_keys(poly_belongs_to_klass)
19
+ else
20
+ reflection.join_keys
21
+ end
8
22
  end
9
23
  elsif ActiveRecord.gem_version >= Gem::Version.new("4.2")
10
- def self.join_keys(reflection)
11
- reflection.join_keys(reflection.klass)
24
+ def self.join_keys(reflection, poly_belongs_to_klass)
25
+ reflection.join_keys(poly_belongs_to_klass || reflection.klass)
12
26
  end
13
27
  else
14
28
  # 4.1 change that introduced JoinKeys:
15
29
  # https://github.com/rails/rails/commit/5823e429981dc74f8f53187d2ab573823381bf28#diff-523caff658498027f61cae9d91c8503dL108
16
30
  JoinKeys = Struct.new(:key, :foreign_key)
17
- def self.join_keys(reflection)
31
+ def self.join_keys(reflection, poly_belongs_to_klass)
18
32
  if reflection.source_macro == :belongs_to
19
- # The original code had to handle polymorphic here. But we don't support polymorphic belongs_to
20
- # So the code would never reach here in the polymorphic case.
21
- key = reflection.association_primary_key
33
+ key = reflection.association_primary_key(poly_belongs_to_klass)
22
34
  foreign_key = reflection.foreign_key
23
35
  else
24
36
  key = reflection.foreign_key
@@ -31,7 +43,17 @@ module ActiveRecordWhereAssoc
31
43
 
32
44
  if ActiveRecord.gem_version >= Gem::Version.new("5.0")
33
45
  def self.chained_reflection_and_chained_constraints(reflection)
34
- reflection.chain.map { |ref| [ref, ref.constraints] }.transpose
46
+ pairs = reflection.chain.map do |ref|
47
+ # PolymorphicReflection is a super weird thing. Like a partial reflection, I don't get it.
48
+ # Seems like just bypassing it works for our needs.
49
+ # When doing a has_many through that has a polymorphic source and a source_type, this ends up
50
+ # part of the chain instead of the regular HasManyReflection that one would expect.
51
+ ref = ref.instance_variable_get(:@reflection) if ref.is_a?(ActiveRecord::Reflection::PolymorphicReflection)
52
+
53
+ [ref, ref.constraints]
54
+ end
55
+
56
+ pairs.transpose
35
57
  end
36
58
  else
37
59
  def self.chained_reflection_and_chained_constraints(reflection)
@@ -59,5 +81,15 @@ module ActiveRecordWhereAssoc
59
81
  association_name.to_sym
60
82
  end
61
83
  end
84
+
85
+ if ActiveRecord.gem_version >= Gem::Version.new("5.0")
86
+ def self.through_reflection?(reflection)
87
+ reflection.through_reflection?
88
+ end
89
+ else
90
+ def self.through_reflection?(reflection)
91
+ reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
92
+ end
93
+ end
62
94
  end
63
95
  end
@@ -8,21 +8,39 @@ module ActiveRecordWhereAssoc
8
8
  # Arel table used for aliasing when handling recursive associations (such as parent/children)
9
9
  ALIAS_TABLE = Arel::Table.new("_ar_where_assoc_alias_")
10
10
 
11
- # Block used when nesting associations for a where_assoc_[not_]exists
12
- # Will apply the nested scope to the wrapping_scope with: where("EXISTS (SELECT... *nested_scope*)")
13
- # exists_prefix: raw sql prefix to the EXISTS, ex: 'NOT '
14
- NestWithExistsBlock = lambda do |wrapping_scope, nested_scope, exists_prefix = ""|
15
- sql = "#{exists_prefix}EXISTS (#{nested_scope.select('1').to_sql})"
11
+ # Returns the SQL for checking if any of the received relation exists.
12
+ # Uses a OR if there are multiple relations.
13
+ # => "EXISTS (SELECT... *relation1*) OR EXISTS (SELECT... *relation2*)"
14
+ def self.sql_for_any_exists(relations)
15
+ relations = [relations] unless relations.is_a?(Array)
16
+ relations = relations.reject { |rel| rel.is_a?(ActiveRecord::NullRelation) }
17
+ sqls = relations.map { |rel| "EXISTS (#{rel.select('1').to_sql})" }
18
+ if sqls.size > 1
19
+ "(#{sqls.join(" OR ")})" # Parens needed when embedding the sql in a `where`, because the OR could make things wrong
20
+ elsif sqls.size == 1
21
+ sqls.first
22
+ else
23
+ "0=1"
24
+ end
25
+ end
16
26
 
17
- wrapping_scope.where(sql)
27
+ # Block used when nesting associations for a where_assoc_[not_]exists
28
+ NestWithExistsBlock = lambda do |wrapping_scope, nested_scopes|
29
+ wrapping_scope.where(sql_for_any_exists(nested_scopes))
18
30
  end
19
31
 
20
- # Block used when nesting associations for a where_assoc_count
21
- # Will apply the nested scope to the wrapping_scope with: select("SUM(SELECT... *nested_scope*)")
22
- NestWithSumBlock = lambda do |wrapping_scope, nested_scope|
32
+ # Returns the SQL for getting the sum of of the received relations
33
+ # => "SUM((SELECT... *relation1*)) + SUM((SELECT... *relation2*))"
34
+ def self.sql_for_sum_of_counts(relations)
35
+ relations = [relations] unless relations.is_a?(Array)
36
+ relations = relations.reject { |rel| rel.is_a?(ActiveRecord::NullRelation) }
23
37
  # Need the double parentheses
24
- sql = "SUM((#{nested_scope.to_sql}))"
38
+ relations.map { |rel| "SUM((#{rel.to_sql}))" }.join(" + ").presence || "0"
39
+ end
25
40
 
41
+ # Block used when nesting associations for a where_assoc_count
42
+ NestWithSumBlock = lambda do |wrapping_scope, nested_scopes|
43
+ sql = sql_for_sum_of_counts(nested_scopes)
26
44
  wrapping_scope.unscope(:select).select(sql)
27
45
  end
28
46
 
@@ -39,66 +57,83 @@ module ActiveRecordWhereAssoc
39
57
  options.fetch(key) { ActiveRecordWhereAssoc.default_options[key] }
40
58
  end
41
59
 
42
- # Returns a new relation, which is the result of filtering base_relation
43
- # based on if a record for the specified association of the model exists.
60
+ # Returns the SQL condition to check if the specified association of the record_class exists (has records).
44
61
  #
45
- # See #where_assoc_exists in query_methods.rb for usage details.
46
- def self.do_where_assoc_exists(base_relation, association_name, given_scope, options, &block)
47
- nested_relation = relation_on_association(base_relation, association_name, given_scope, options, block, NestWithExistsBlock)
48
- NestWithExistsBlock.call(base_relation, nested_relation)
62
+ # See RelationReturningMethods#where_assoc_exists or SqlReturningMethods#assoc_exists_sql for usage details.
63
+ def self.assoc_exists_sql(record_class, association_names, given_conditions, options, &block)
64
+ nested_relations = relations_on_association(record_class, association_names, given_conditions, options, block, NestWithExistsBlock)
65
+ sql_for_any_exists(nested_relations)
49
66
  end
50
67
 
51
- # Returns a new relation, which is the result of filtering base_relation
52
- # based on if a record for the specified association of the model doesn't exist.
68
+ # Returns the SQL condition to check if the specified association of the record_class doesn't exist (has no records).
53
69
  #
54
- # See #where_assoc_exists in query_methods.rb for usage details.
55
- def self.do_where_assoc_not_exists(base_relation, association_name, given_scope, options, &block)
56
- nested_relation = relation_on_association(base_relation, association_name, given_scope, options, block, NestWithExistsBlock)
57
- NestWithExistsBlock.call(base_relation, nested_relation, "NOT ")
70
+ # See RelationReturningMethods#where_assoc_not_exists or SqlReturningMethods#assoc_not_exists_sql for usage details.
71
+ def self.assoc_not_exists_sql(record_class, association_names, given_conditions, options, &block)
72
+ nested_relations = relations_on_association(record_class, association_names, given_conditions, options, block, NestWithExistsBlock)
73
+ "NOT #{sql_for_any_exists(nested_relations)}"
58
74
  end
59
75
 
60
- # Returns a new relation, which is the result of filtering base_relation
61
- # based on how many records for the specified association of the model exists.
76
+ # This does not return an SQL condition. Instead, it returns only the SQL to count the number of records for the specified
77
+ # association.
62
78
  #
63
- # See #where_assoc_exists and #where_assoc_count in query_methods.rb for usage details.
64
- def self.do_where_assoc_count(base_relation, left_operand, operator, association_name, given_scope, options, &block)
79
+ # See SqlReturningMethods#only_assoc_count_sql for usage details.
80
+ def self.only_assoc_count_sql(record_class, association_names, given_conditions, options, &block)
65
81
  deepest_scope_mod = lambda do |deepest_scope|
66
82
  deepest_scope = apply_proc_scope(deepest_scope, block) if block
67
83
 
68
84
  deepest_scope.unscope(:select).select("COUNT(*)")
69
85
  end
70
86
 
71
- nested_relation = relation_on_association(base_relation, association_name, given_scope, options, deepest_scope_mod, NestWithSumBlock)
87
+ nested_relations = relations_on_association(record_class, association_names, given_conditions, options, deepest_scope_mod, NestWithSumBlock)
88
+ nested_relations = nested_relations.reject { |rel| rel.is_a?(ActiveRecord::NullRelation) }
89
+ nested_relations.map { |nr| "COALESCE((#{nr.to_sql}), 0)" }.join(" + ").presence || "0"
90
+ end
91
+
92
+ # Returns the SQL condition to check if the specified association of the record_class has the desired number of records.
93
+ #
94
+ # See RelationReturningMethods#where_assoc_count or SqlReturningMethods#compare_assoc_count_sql for usage details.
95
+ def self.compare_assoc_count_sql(record_class, left_operand, operator, association_names, given_conditions, options, &block)
96
+ right_sql = only_assoc_count_sql(record_class, association_names, given_conditions, options, &block)
72
97
 
73
- sql = sql_for_count_operator(left_operand, operator, "COALESCE((#{nested_relation.to_sql}), 0)")
74
- base_relation.where(sql)
98
+ sql_for_count_operator(left_operand, operator, right_sql)
75
99
  end
76
100
 
77
- # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
78
- # association_names_path: can be an array of association names or a single one
79
- def self.relation_on_association(base_relation, association_names_path, given_scope, options, last_assoc_block, nest_assocs_block)
101
+ # Returns relations on the associated model meant to be embedded in a query
102
+ # Will only return more than one association when there are polymorphic belongs_to
103
+ # association_names: can be an array of association names or a single one
104
+ def self.relations_on_association(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block)
80
105
  validate_options(options)
81
- association_names_path = Array.wrap(association_names_path)
106
+ association_names = Array.wrap(association_names)
107
+ _relations_on_association_recurse(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block)
108
+ end
82
109
 
83
- if association_names_path.size > 1
110
+ def self._relations_on_association_recurse(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block)
111
+ if association_names.size > 1
84
112
  recursive_scope_block = lambda do |scope|
85
- nested_scope = relation_on_association(scope, association_names_path[1..-1], given_scope, options, last_assoc_block, nest_assocs_block)
113
+ nested_scope = _relations_on_association_recurse(scope,
114
+ association_names[1..-1],
115
+ given_conditions,
116
+ options,
117
+ last_assoc_block,
118
+ nest_assocs_block)
86
119
  nest_assocs_block.call(scope, nested_scope)
87
120
  end
88
121
 
89
- relation_on_one_association(base_relation, association_names_path.first, nil, options, recursive_scope_block, nest_assocs_block)
122
+ relations_on_one_association(record_class, association_names.first, nil, options, recursive_scope_block, nest_assocs_block)
90
123
  else
91
- relation_on_one_association(base_relation, association_names_path.first, given_scope, options, last_assoc_block, nest_assocs_block)
124
+ relations_on_one_association(record_class, association_names.first, given_conditions, options, last_assoc_block, nest_assocs_block)
92
125
  end
93
126
  end
94
127
 
95
- # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
96
- def self.relation_on_one_association(base_relation, association_name, given_scope, options, last_assoc_block, nest_assocs_block)
97
- relation_klass = base_relation.klass
98
- final_reflection = fetch_reflection(relation_klass, association_name)
128
+ # Returns relations on the associated model meant to be embedded in a query
129
+ # Will return more than one association only for polymorphic belongs_to
130
+ def self.relations_on_one_association(record_class, association_name, given_conditions, options, last_assoc_block, nest_assocs_block)
131
+ final_reflection = fetch_reflection(record_class, association_name)
132
+
133
+ check_reflection_validity!(final_reflection)
99
134
 
100
- nested_scope = nil
101
- current_scope = nil
135
+ nested_scopes = nil
136
+ current_scopes = nil
102
137
 
103
138
  # Chain deals with through stuff
104
139
  # We will start with the reflection that points on the final model, and slowly move back to the reflection
@@ -118,24 +153,35 @@ module ActiveRecordWhereAssoc
118
153
  # the 2nd part of has_and_belongs_to_many is handled at the same time as the first.
119
154
  skip_next = true if actually_has_and_belongs_to_many?(reflection)
120
155
 
121
- wrapper_scope, current_scope = initial_scope_from_reflection(reflection_chain[i..-1], constaints_chain[i])
122
-
123
- current_scope = process_association_step_limits(current_scope, reflection, relation_klass, options)
124
-
125
- if i.zero?
126
- current_scope = current_scope.where(given_scope) if given_scope
127
- current_scope = apply_proc_scope(current_scope, last_assoc_block) if last_assoc_block
156
+ init_scopes = initial_scopes_from_reflection(reflection_chain[i..-1], constaints_chain[i], options)
157
+ current_scopes = init_scopes.map do |alias_scope, current_scope, klass_scope|
158
+ current_scope = process_association_step_limits(current_scope, reflection, record_class, options)
159
+
160
+ if i.zero?
161
+ current_scope = current_scope.where(given_conditions) if given_conditions
162
+ if klass_scope
163
+ if klass_scope.respond_to?(:call)
164
+ current_scope = apply_proc_scope(current_scope, klass_scope)
165
+ else
166
+ current_scope = current_scope.where(klass_scope)
167
+ end
168
+ end
169
+ current_scope = apply_proc_scope(current_scope, last_assoc_block) if last_assoc_block
170
+ end
171
+
172
+ # Those make no sense since at this point, we are only limiting the value that would match using conditions
173
+ # Those could have been added by the received block, so just remove them
174
+ current_scope = current_scope.unscope(:limit, :order, :offset)
175
+
176
+ current_scope = nest_assocs_block.call(current_scope, nested_scopes) if nested_scopes
177
+ current_scope = nest_assocs_block.call(alias_scope, current_scope) if alias_scope
178
+ current_scope
128
179
  end
129
180
 
130
- # Those make no sense since we are only limiting the value that would match, using conditions
131
- current_scope = current_scope.unscope(:limit, :order, :offset)
132
- current_scope = nest_assocs_block.call(current_scope, nested_scope) if nested_scope
133
- current_scope = nest_assocs_block.call(wrapper_scope, current_scope) if wrapper_scope
134
-
135
- nested_scope = current_scope
181
+ nested_scopes = current_scopes
136
182
  end
137
183
 
138
- current_scope
184
+ current_scopes
139
185
  end
140
186
 
141
187
  def self.fetch_reflection(relation_klass, association_name)
@@ -146,68 +192,75 @@ module ActiveRecordWhereAssoc
146
192
  # Need to use build because this exception expects a record...
147
193
  raise ActiveRecord::AssociationNotFoundError.new(relation_klass.new, association_name)
148
194
  end
149
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
150
- # TODO: We might want an option to indicate that using pluck is ok?
151
- raise NotImplementedError, "Can't deal with polymorphic belongs_to"
152
- end
153
195
 
154
196
  reflection
155
197
  end
156
198
 
157
- def self.initial_scope_from_reflection(reflection_chain, constraints)
199
+ # Can return multiple pairs for polymorphic belongs_to, one per table to look into
200
+ def self.initial_scopes_from_reflection(reflection_chain, assoc_scopes, options)
158
201
  reflection = reflection_chain.first
159
- current_scope = reflection.klass.default_scoped
160
-
161
- if actually_has_and_belongs_to_many?(reflection)
162
- # has_and_belongs_to_many, behind the scene has a secret model and uses a has_many through.
163
- # This is the first of those two secret has_many through.
164
- #
165
- # In order to handle limit, offset, order correctly on has_and_belongs_to_many,
166
- # we must do both this reflection and the next one at the same time.
167
- # Think of it this way, if you have limit 3:
168
- # Apply only on 1st step: You check that any of 2nd step for the first 3 of 1st step match
169
- # Apply only on 2nd step: You check that any of the first 3 of second step match for any 1st step
170
- # Apply over both (as we do): You check that only the first 3 of doing both step match,
171
-
172
- # To create the join, simply using next_reflection.klass.default_scoped.joins(reflection.name)
173
- # would be great, except we cannot add a given_scope afterward because we are on the wrong "base class",
174
- # and we can't do #merge because of the LEW crap.
175
- # So we must do the joins ourself!
176
- _wrapper, sub_join_contraints = wrapper_and_join_constraints(reflection)
177
- next_reflection = reflection_chain[1]
178
-
179
- current_scope = current_scope.joins(<<-SQL)
180
- INNER JOIN #{next_reflection.klass.quoted_table_name} ON #{sub_join_contraints.to_sql}
181
- SQL
182
-
183
- wrapper_scope, join_constaints = wrapper_and_join_constraints(next_reflection, habtm_other_reflection: reflection)
184
- else
185
- wrapper_scope, join_constaints = wrapper_and_join_constraints(reflection)
186
- end
187
-
188
- constraint_allowed_lim_off = constraint_allowed_lim_off_from(reflection)
189
-
190
- constraints.each do |callable|
191
- relation = reflection.klass.unscoped.instance_exec(&callable)
202
+ actual_source_reflection = user_defined_actual_source_reflection(reflection)
203
+
204
+ on_poly_belongs_to = option_value(options, :poly_belongs_to) if poly_belongs_to?(actual_source_reflection)
205
+
206
+ classes_with_scope = classes_with_scope_for_reflection(reflection, options)
207
+
208
+ assoc_scope_allowed_lim_off = assoc_scope_to_keep_lim_off_from(reflection)
209
+
210
+ classes_with_scope.map do |klass, klass_scope|
211
+ current_scope = klass.default_scoped
212
+
213
+ if actually_has_and_belongs_to_many?(actual_source_reflection)
214
+ # has_and_belongs_to_many, behind the scene has a secret model and uses a has_many through.
215
+ # This is the first of those two secret has_many through.
216
+ #
217
+ # In order to handle limit, offset, order correctly on has_and_belongs_to_many,
218
+ # we must do both this reflection and the next one at the same time.
219
+ # Think of it this way, if you have limit 3:
220
+ # Apply only on 1st step: You check that any of 2nd step for the first 3 of 1st step match
221
+ # Apply only on 2nd step: You check that any of the first 3 of second step match for any 1st step
222
+ # Apply over both (as we do): You check that only the first 3 of doing both step match,
223
+
224
+ # To create the join, simply using next_reflection.klass.default_scoped.joins(reflection.name)
225
+ # would be great, except we cannot add a given_conditions afterward because we are on the wrong "base class",
226
+ # and we can't do #merge because of the LEW crap.
227
+ # So we must do the joins ourself!
228
+ _wrapper, sub_join_contraints = wrapper_and_join_constraints(reflection)
229
+ next_reflection = reflection_chain[1]
230
+
231
+ current_scope = current_scope.joins(<<-SQL)
232
+ INNER JOIN #{next_reflection.klass.quoted_table_name} ON #{sub_join_contraints.to_sql}
233
+ SQL
234
+
235
+ alias_scope, join_constaints = wrapper_and_join_constraints(next_reflection, habtm_other_reflection: reflection)
236
+ elsif on_poly_belongs_to
237
+ alias_scope, join_constaints = wrapper_and_join_constraints(reflection, poly_belongs_to_klass: klass)
238
+ else
239
+ alias_scope, join_constaints = wrapper_and_join_constraints(reflection)
240
+ end
192
241
 
193
- if callable != constraint_allowed_lim_off
194
- # I just want to remove the current values without screwing things in the merge below
195
- # so we cannot use #unscope
196
- relation.limit_value = nil
197
- relation.offset_value = nil
198
- relation.order_values = []
242
+ assoc_scopes.each do |callable|
243
+ relation = klass.unscoped.instance_exec(nil, &callable)
244
+
245
+ if callable != assoc_scope_allowed_lim_off
246
+ # I just want to remove the current values without screwing things in the merge below
247
+ # so we cannot use #unscope
248
+ relation.limit_value = nil
249
+ relation.offset_value = nil
250
+ relation.order_values = []
251
+ end
252
+
253
+ # Need to use merge to replicate the Last Equality Wins behavior of associations
254
+ # https://github.com/rails/rails/issues/7365
255
+ # See also the test/tests/wa_last_equality_wins_test.rb for an explanation
256
+ current_scope = current_scope.merge(relation)
199
257
  end
200
258
 
201
- # Need to use merge to replicate the Last Equality Wins behavior of associations
202
- # https://github.com/rails/rails/issues/7365
203
- # See also the test/tests/wa_last_equality_wins_test.rb for an explanation
204
- current_scope = current_scope.merge(relation)
259
+ [alias_scope, current_scope.where(join_constaints), klass_scope]
205
260
  end
206
-
207
- [wrapper_scope, current_scope.where(join_constaints)]
208
261
  end
209
262
 
210
- def self.constraint_allowed_lim_off_from(reflection)
263
+ def self.assoc_scope_to_keep_lim_off_from(reflection)
211
264
  # For :through associations, it's pretty hard/tricky to apply limit/offset/order of the
212
265
  # whole has_* :through. For now, we only apply those of the direct associations from one model
213
266
  # to another that the :through uses and we ignore the limit/offset/order from the scope of has_* :through.
@@ -219,12 +272,63 @@ module ActiveRecordWhereAssoc
219
272
  user_defined_actual_source_reflection(reflection).scope
220
273
  end
221
274
 
275
+ def self.classes_with_scope_for_reflection(reflection, options)
276
+ actual_source_reflection = user_defined_actual_source_reflection(reflection)
277
+
278
+ if poly_belongs_to?(actual_source_reflection)
279
+ on_poly_belongs_to = option_value(options, :poly_belongs_to)
280
+
281
+ if reflection.options[:source_type]
282
+ [reflection.options[:source_type].safe_constantize].compact
283
+ else
284
+ case on_poly_belongs_to
285
+ when :pluck
286
+ class_names = actual_source_reflection.active_record.distinct.pluck(actual_source_reflection.foreign_type)
287
+ class_names.compact.map!(&:safe_constantize).compact
288
+ when Array, Hash
289
+ array = on_poly_belongs_to.to_a
290
+ bad_class = array.detect { |c, _p| !c.is_a?(Class) || !(c < ActiveRecord::Base) }
291
+ if bad_class.is_a?(ActiveRecord::Base)
292
+ raise ArgumentError, "Must receive the Class of the model, not an instance. This is wrong: #{bad_class.inspect}"
293
+ elsif bad_class
294
+ raise ArgumentError, "Expected #{bad_class.inspect} to be a subclass of ActiveRecord::Base"
295
+ end
296
+ array
297
+ when :raise
298
+ msg = String.new
299
+ if actual_source_reflection == reflection
300
+ msg << "Association #{reflection.name.inspect} is a polymorphic belongs_to. "
301
+ else
302
+ msg << "Association #{reflection.name.inspect} is a :through relation that uses a polymorphic belongs_to"
303
+ msg << "#{actual_source_reflection.name.inspect} as source without without a source_type. "
304
+ end
305
+ msg << "This is not supported by ActiveRecord when doing joins, but it is by WhereAssoc. However, "
306
+ msg << "you must pass the :poly_belongs_to option to specify what to do in this case.\n"
307
+ msg << "See https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-3Apoly_belongs_to+option"
308
+ raise ActiveRecordWhereAssoc::PolymorphicBelongsToWithoutClasses, msg
309
+ else
310
+ if on_poly_belongs_to.is_a?(Class) && on_poly_belongs_to < ActiveRecord::Base
311
+ [on_poly_belongs_to]
312
+ else
313
+ raise ArgumentError, "Received a bad value for :poly_belongs_to: #{on_poly_belongs_to.inspect}"
314
+ end
315
+ end
316
+ end
317
+ else
318
+ [reflection.klass]
319
+ end
320
+ end
321
+
322
+ # Creates a sub_query that the current_scope gets nested into if there is limit/offset to apply
222
323
  def self.process_association_step_limits(current_scope, reflection, relation_klass, options)
223
- return current_scope.unscope(:limit, :offset, :order) if user_defined_actual_source_reflection(reflection).macro == :belongs_to
324
+ if user_defined_actual_source_reflection(reflection).macro == :belongs_to || option_value(options, :ignore_limit)
325
+ return current_scope.unscope(:limit, :offset, :order)
326
+ end
224
327
 
225
- current_scope = current_scope.limit(1) if reflection.macro == :has_one
328
+ # No need to do transformations if this is already a NullRelation
329
+ return current_scope if current_scope.is_a?(ActiveRecord::NullRelation)
226
330
 
227
- current_scope = current_scope.unscope(:limit, :offset) if option_value(options, :ignore_limit)
331
+ current_scope = current_scope.limit(1) if reflection.macro == :has_one
228
332
 
229
333
  # Order is useless without either limit or offset
230
334
  current_scope = current_scope.unscope(:order) if !current_scope.limit_value && !current_scope.offset_value
@@ -234,7 +338,7 @@ module ActiveRecordWhereAssoc
234
338
  msg = String.new
235
339
  msg << "Associations and default_scopes with a limit or offset are not supported for MySQL (this includes has_many). "
236
340
  msg << "Use ignore_limit: true to ignore both limit and offset, and treat has_one like has_many. "
237
- msg << "See https://github.com/MaxLap/activerecord_where_assoc/tree/ignore_limits#mysql-doesnt-support-sub-limit for details."
341
+ msg << "See https://github.com/MaxLap/activerecord_where_assoc#mysql-doesnt-support-sub-limit for details."
238
342
  raise MySQLDoesntSupportSubLimitError, msg
239
343
  end
240
344
 
@@ -250,10 +354,7 @@ module ActiveRecordWhereAssoc
250
354
  # be useful.
251
355
 
252
356
  if reflection.klass.table_name.include?(".") || option_value(options, :never_alias_limit)
253
- # This works universally, but seems to sometimes have slower performances.. Need to test if there is an alternative way
254
- # of expressing this...
255
- # TODO: Investigate a way to improve performances, or maybe require a flag to do it this way?
256
- # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless if it
357
+ # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless it
257
358
  # could helps the query planner of the DB, if someone can show it to be worth it, then this can be changed.)
258
359
 
259
360
  reflection.klass.unscoped.where(reflection.klass.primary_key.to_sym => current_scope)
@@ -272,30 +373,32 @@ module ActiveRecordWhereAssoc
272
373
  # If it can receive arguments, call the proc the relation passed as argument
273
374
  def self.apply_proc_scope(relation, proc_scope)
274
375
  if proc_scope.arity == 0
275
- relation.instance_exec(&proc_scope) || relation
376
+ relation.instance_exec(nil, &proc_scope) || relation
276
377
  else
277
378
  proc_scope.call(relation) || relation
278
379
  end
279
380
  end
280
381
 
281
- def self.build_wrapper_scope_for_recursive_association(reflection)
282
- table = reflection.klass.arel_table
283
- primary_key = reflection.klass.primary_key
382
+ def self.build_alias_scope_for_recursive_association(reflection, poly_belongs_to_klass)
383
+ klass = poly_belongs_to_klass || reflection.klass
384
+ table = klass.arel_table
385
+ primary_key = klass.primary_key
284
386
  foreign_klass = reflection.send(:actual_source_reflection).active_record
285
387
 
286
- wrapper_scope = foreign_klass.base_class.unscoped
287
- wrapper_scope = wrapper_scope.from("#{table.name} #{ALIAS_TABLE.name}")
288
- wrapper_scope = wrapper_scope.where(table[primary_key].eq(ALIAS_TABLE[primary_key]))
289
- wrapper_scope
388
+ alias_scope = foreign_klass.base_class.unscoped
389
+ alias_scope = alias_scope.from("#{table.name} #{ALIAS_TABLE.name}")
390
+ alias_scope = alias_scope.where(table[primary_key].eq(ALIAS_TABLE[primary_key]))
391
+ alias_scope
290
392
  end
291
393
 
292
394
  def self.wrapper_and_join_constraints(reflection, options = {})
293
- join_keys = ActiveRecordCompat.join_keys(reflection)
395
+ poly_belongs_to_klass = options[:poly_belongs_to_klass]
396
+ join_keys = ActiveRecordCompat.join_keys(reflection, poly_belongs_to_klass)
294
397
 
295
398
  key = join_keys.key
296
399
  foreign_key = join_keys.foreign_key
297
400
 
298
- table = reflection.klass.arel_table
401
+ table = (poly_belongs_to_klass || reflection.klass).arel_table
299
402
  foreign_klass = reflection.send(:actual_source_reflection).active_record
300
403
  foreign_table = foreign_klass.arel_table
301
404
 
@@ -303,25 +406,36 @@ module ActiveRecordWhereAssoc
303
406
  habtm_other_table = habtm_other_reflection.klass.arel_table if habtm_other_reflection
304
407
 
305
408
  if (habtm_other_table || table).name == foreign_table.name
306
- wrapper_scope = build_wrapper_scope_for_recursive_association(habtm_other_reflection || reflection)
409
+ alias_scope = build_alias_scope_for_recursive_association(habtm_other_reflection || reflection, poly_belongs_to_klass)
307
410
  foreign_table = ALIAS_TABLE
308
411
  end
309
412
 
310
413
  constraints = table[key].eq(foreign_table[foreign_key])
311
414
 
312
415
  if reflection.type
313
- # Handing of the polymorphic has_many/has_one's type column
416
+ # Handling of the polymorphic has_many/has_one's type column
314
417
  constraints = constraints.and(table[reflection.type].eq(foreign_klass.base_class.name))
315
418
  end
316
419
 
317
- [wrapper_scope, constraints]
420
+ if poly_belongs_to_klass
421
+ constraints = constraints.and(foreign_table[reflection.foreign_type].eq(poly_belongs_to_klass.base_class.name))
422
+ end
423
+
424
+ [alias_scope, constraints]
318
425
  end
319
426
 
427
+ # Because we work using Model._reflections, we don't actually get the :has_and_belongs_to_many.
428
+ # Instead, we get a has_many :through, which is was ActiveRecord created behind the scene.
429
+ # This code detects that a :through is actually a has_and_belongs_to_many.
320
430
  def self.has_and_belongs_to_many?(reflection) # rubocop:disable Naming/PredicateName
321
431
  parent = ActiveRecordCompat.parent_reflection(reflection)
322
432
  parent && parent.macro == :has_and_belongs_to_many
323
433
  end
324
434
 
435
+ def self.poly_belongs_to?(reflection)
436
+ reflection.macro == :belongs_to && reflection.options[:polymorphic]
437
+ end
438
+
325
439
  # Return true if #user_defined_actual_source_reflection is a has_and_belongs_to_many
326
440
  def self.actually_has_and_belongs_to_many?(reflection)
327
441
  has_and_belongs_to_many?(user_defined_actual_source_reflection(reflection))
@@ -339,6 +453,26 @@ module ActiveRecordWhereAssoc
339
453
  end
340
454
  end
341
455
 
456
+ def self.check_reflection_validity!(reflection)
457
+ if ActiveRecordCompat.through_reflection?(reflection)
458
+ # Copied from ActiveRecord
459
+ if reflection.through_reflection.polymorphic?
460
+ # Since deep_cover/builtin_takeover lacks some granularity,
461
+ # it can sometimes happen that it won't display 100% coverage while a regular would
462
+ # be 100%. This happens when multiple banches are on in a single line.
463
+ # For this reason, I split this condition in 2
464
+ if ActiveRecord.const_defined?(:HasOneAssociationPolymorphicThroughError)
465
+ if reflection.has_one?
466
+ raise ActiveRecord::HasOneAssociationPolymorphicThroughError.new(reflection.active_record.name, reflection)
467
+ end
468
+ end
469
+ raise ActiveRecord::HasManyThroughAssociationPolymorphicThroughError.new(reflection.active_record.name, reflection)
470
+ end
471
+ check_reflection_validity!(reflection.through_reflection)
472
+ check_reflection_validity!(reflection.source_reflection)
473
+ end
474
+ end
475
+
342
476
  # Doing (SQL) BETWEEN v1 AND v2, where v2 is infinite means (SQL) >= v1. However,
343
477
  # we place the SQL on the right side, so the operator is flipped to become v1 <= (SQL).
344
478
  # Doing (SQL) NOT BETWEEN v1 AND v2 where v2 is infinite means (SQL) < v1. However,
@@ -368,11 +502,12 @@ module ActiveRecordWhereAssoc
368
502
  v1 = left_operand.begin
369
503
  v2 = left_operand.end || Float::INFINITY
370
504
 
505
+ # We are doing a count and summing them, the lowest possible is 0, so just use that instead of changing the SQL used.
371
506
  v1 = 0 if v1 == -Float::INFINITY
372
507
 
373
508
  return sql_for_count_operator(v1, RIGHT_INFINITE_RANGE_OPERATOR_MAP.fetch(operator), right_sql) if v2 == Float::INFINITY
374
509
 
375
- # Its int or a float with no mantissa, exclude_end? means -1
510
+ # Its int or a rounded float. Since we are comparing to integer values (count), exclude_end? just means -1
376
511
  v2 -= 1 if left_operand.exclude_end? && v2 % 1 == 0
377
512
 
378
513
  "#{right_sql} #{RANGE_OPERATOR_MAP.fetch(operator)} #{v1} AND #{v2}"