activerecord_where_assoc 0.1.3 → 1.1.2

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.
@@ -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}"