activerecord_where_assoc 0.1.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,17 +11,21 @@ module ActiveRecordWhereAssoc
11
11
  # Block used when nesting associations for a where_assoc_[not_]exists
12
12
  # Will apply the nested scope to the wrapping_scope with: where("EXISTS (SELECT... *nested_scope*)")
13
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})"
14
+ NestWithExistsBlock = lambda do |wrapping_scope, nested_scopes, exists_prefix = ""|
15
+ nested_scopes = [nested_scopes] unless nested_scopes.is_a?(Array)
16
+ sql = nested_scopes.map { |ns| "EXISTS (#{ns.select('1').to_sql})" }.join(" OR ")
17
+ sql = "(#{sql})" if nested_scopes.size > 1
18
+ sql = sql.presence || "0=1"
16
19
 
17
- wrapping_scope.where(sql)
20
+ wrapping_scope.where(exists_prefix + sql)
18
21
  end
19
22
 
20
23
  # Block used when nesting associations for a where_assoc_count
21
24
  # Will apply the nested scope to the wrapping_scope with: select("SUM(SELECT... *nested_scope*)")
22
- NestWithSumBlock = lambda do |wrapping_scope, nested_scope|
25
+ NestWithSumBlock = lambda do |wrapping_scope, nested_scopes|
26
+ nested_scopes = [nested_scopes] unless nested_scopes.is_a?(Array)
23
27
  # Need the double parentheses
24
- sql = "SUM((#{nested_scope.to_sql}))"
28
+ sql = nested_scopes.map { |ns| "SUM((#{ns.to_sql}))" }.join(" + ").presence || "0"
25
29
 
26
30
  wrapping_scope.unscope(:select).select(sql)
27
31
  end
@@ -43,62 +47,73 @@ module ActiveRecordWhereAssoc
43
47
  # based on if a record for the specified association of the model exists.
44
48
  #
45
49
  # 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)
50
+ def self.do_where_assoc_exists(base_relation, association_name, given_conditions, options, &block)
51
+ nested_relations = relations_on_association(base_relation, association_name, given_conditions, options, block, NestWithExistsBlock)
52
+ NestWithExistsBlock.call(base_relation, nested_relations)
49
53
  end
50
54
 
51
55
  # Returns a new relation, which is the result of filtering base_relation
52
56
  # based on if a record for the specified association of the model doesn't exist.
53
57
  #
54
58
  # 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 ")
59
+ def self.do_where_assoc_not_exists(base_relation, association_name, given_conditions, options, &block)
60
+ nested_relations = relations_on_association(base_relation, association_name, given_conditions, options, block, NestWithExistsBlock)
61
+ NestWithExistsBlock.call(base_relation, nested_relations, "NOT ")
58
62
  end
59
63
 
60
64
  # Returns a new relation, which is the result of filtering base_relation
61
65
  # based on how many records for the specified association of the model exists.
62
66
  #
63
67
  # 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)
68
+ def self.do_where_assoc_count(base_relation, left_operand, operator, association_name, given_conditions, options, &block)
65
69
  deepest_scope_mod = lambda do |deepest_scope|
66
70
  deepest_scope = apply_proc_scope(deepest_scope, block) if block
67
71
 
68
72
  deepest_scope.unscope(:select).select("COUNT(*)")
69
73
  end
70
74
 
71
- nested_relation = relation_on_association(base_relation, association_name, given_scope, options, deepest_scope_mod, NestWithSumBlock)
75
+ nested_relations = relations_on_association(base_relation, association_name, given_conditions, options, deepest_scope_mod, NestWithSumBlock)
72
76
 
73
- sql = sql_for_count_operator(left_operand, operator, "COALESCE((#{nested_relation.to_sql}), 0)")
77
+ right_sql = nested_relations.map { |nr| "COALESCE((#{nr.to_sql}), 0)" }.join(" + ").presence || "0"
78
+
79
+ sql = sql_for_count_operator(left_operand, operator, right_sql)
74
80
  base_relation.where(sql)
75
81
  end
76
82
 
77
- # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
83
+ # Returns relations on the associated model meant to be embedded in a query
84
+ # Will return more than one association only for polymorphic belongs_to
78
85
  # 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)
86
+ def self.relations_on_association(base_relation, association_names_path, given_conditions, options, last_assoc_block, nest_assocs_block)
80
87
  validate_options(options)
81
88
  association_names_path = Array.wrap(association_names_path)
82
89
 
83
90
  if association_names_path.size > 1
84
91
  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)
92
+ nested_scope = relations_on_association(scope,
93
+ association_names_path[1..-1],
94
+ given_conditions,
95
+ options,
96
+ last_assoc_block,
97
+ nest_assocs_block)
86
98
  nest_assocs_block.call(scope, nested_scope)
87
99
  end
88
100
 
89
- relation_on_one_association(base_relation, association_names_path.first, nil, options, recursive_scope_block, nest_assocs_block)
101
+ relations_on_one_association(base_relation, association_names_path.first, nil, options, recursive_scope_block, nest_assocs_block)
90
102
  else
91
- relation_on_one_association(base_relation, association_names_path.first, given_scope, options, last_assoc_block, nest_assocs_block)
103
+ relations_on_one_association(base_relation, association_names_path.first, given_conditions, options, last_assoc_block, nest_assocs_block)
92
104
  end
93
105
  end
94
106
 
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)
107
+ # Returns relations on the associated model meant to be embedded in a query
108
+ # Will return more than one association only for polymorphic belongs_to
109
+ def self.relations_on_one_association(base_relation, association_name, given_conditions, options, last_assoc_block, nest_assocs_block)
97
110
  relation_klass = base_relation.klass
98
111
  final_reflection = fetch_reflection(relation_klass, association_name)
99
112
 
100
- nested_scope = nil
101
- current_scope = nil
113
+ check_reflection_validity!(final_reflection)
114
+
115
+ nested_scopes = nil
116
+ current_scopes = nil
102
117
 
103
118
  # Chain deals with through stuff
104
119
  # We will start with the reflection that points on the final model, and slowly move back to the reflection
@@ -118,24 +133,35 @@ module ActiveRecordWhereAssoc
118
133
  # the 2nd part of has_and_belongs_to_many is handled at the same time as the first.
119
134
  skip_next = true if actually_has_and_belongs_to_many?(reflection)
120
135
 
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
136
+ init_scopes = initial_scopes_from_reflection(reflection_chain[i..-1], constaints_chain[i], options)
137
+ current_scopes = init_scopes.map do |alias_scope, current_scope, klass_scope|
138
+ current_scope = process_association_step_limits(current_scope, reflection, relation_klass, options)
139
+
140
+ if i.zero?
141
+ current_scope = current_scope.where(given_conditions) if given_conditions
142
+ if klass_scope
143
+ if klass_scope.respond_to?(:call)
144
+ current_scope = apply_proc_scope(current_scope, klass_scope)
145
+ else
146
+ current_scope = current_scope.where(klass_scope)
147
+ end
148
+ end
149
+ current_scope = apply_proc_scope(current_scope, last_assoc_block) if last_assoc_block
150
+ end
151
+
152
+ # Those make no sense since at this point, we are only limiting the value that would match using conditions
153
+ # Those could have been added by the received block, so just remove them
154
+ current_scope = current_scope.unscope(:limit, :order, :offset)
155
+
156
+ current_scope = nest_assocs_block.call(current_scope, nested_scopes) if nested_scopes
157
+ current_scope = nest_assocs_block.call(alias_scope, current_scope) if alias_scope
158
+ current_scope
128
159
  end
129
160
 
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
161
+ nested_scopes = current_scopes
136
162
  end
137
163
 
138
- current_scope
164
+ current_scopes
139
165
  end
140
166
 
141
167
  def self.fetch_reflection(relation_klass, association_name)
@@ -146,68 +172,75 @@ module ActiveRecordWhereAssoc
146
172
  # Need to use build because this exception expects a record...
147
173
  raise ActiveRecord::AssociationNotFoundError.new(relation_klass.new, association_name)
148
174
  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
175
 
154
176
  reflection
155
177
  end
156
178
 
157
- def self.initial_scope_from_reflection(reflection_chain, constraints)
179
+ # Can return multiple pairs for polymorphic belongs_to, one per table to look into
180
+ def self.initial_scopes_from_reflection(reflection_chain, assoc_scopes, options)
158
181
  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)
182
+ actual_source_reflection = user_defined_actual_source_reflection(reflection)
183
+
184
+ on_poly_belongs_to = option_value(options, :poly_belongs_to) if poly_belongs_to?(actual_source_reflection)
185
+
186
+ classes_with_scope = classes_with_scope_for_reflection(reflection, options)
187
+
188
+ assoc_scope_allowed_lim_off = assoc_scope_to_keep_lim_off_from(reflection)
189
+
190
+ classes_with_scope.map do |klass, klass_scope|
191
+ current_scope = klass.default_scoped
192
+
193
+ if actually_has_and_belongs_to_many?(actual_source_reflection)
194
+ # has_and_belongs_to_many, behind the scene has a secret model and uses a has_many through.
195
+ # This is the first of those two secret has_many through.
196
+ #
197
+ # In order to handle limit, offset, order correctly on has_and_belongs_to_many,
198
+ # we must do both this reflection and the next one at the same time.
199
+ # Think of it this way, if you have limit 3:
200
+ # Apply only on 1st step: You check that any of 2nd step for the first 3 of 1st step match
201
+ # Apply only on 2nd step: You check that any of the first 3 of second step match for any 1st step
202
+ # Apply over both (as we do): You check that only the first 3 of doing both step match,
203
+
204
+ # To create the join, simply using next_reflection.klass.default_scoped.joins(reflection.name)
205
+ # would be great, except we cannot add a given_conditions afterward because we are on the wrong "base class",
206
+ # and we can't do #merge because of the LEW crap.
207
+ # So we must do the joins ourself!
208
+ _wrapper, sub_join_contraints = wrapper_and_join_constraints(reflection)
209
+ next_reflection = reflection_chain[1]
210
+
211
+ current_scope = current_scope.joins(<<-SQL)
212
+ INNER JOIN #{next_reflection.klass.quoted_table_name} ON #{sub_join_contraints.to_sql}
213
+ SQL
214
+
215
+ alias_scope, join_constaints = wrapper_and_join_constraints(next_reflection, habtm_other_reflection: reflection)
216
+ elsif on_poly_belongs_to
217
+ alias_scope, join_constaints = wrapper_and_join_constraints(reflection, poly_belongs_to_klass: klass)
218
+ else
219
+ alias_scope, join_constaints = wrapper_and_join_constraints(reflection)
220
+ end
192
221
 
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 = []
222
+ assoc_scopes.each do |callable|
223
+ relation = klass.unscoped.instance_exec(nil, &callable)
224
+
225
+ if callable != assoc_scope_allowed_lim_off
226
+ # I just want to remove the current values without screwing things in the merge below
227
+ # so we cannot use #unscope
228
+ relation.limit_value = nil
229
+ relation.offset_value = nil
230
+ relation.order_values = []
231
+ end
232
+
233
+ # Need to use merge to replicate the Last Equality Wins behavior of associations
234
+ # https://github.com/rails/rails/issues/7365
235
+ # See also the test/tests/wa_last_equality_wins_test.rb for an explanation
236
+ current_scope = current_scope.merge(relation)
199
237
  end
200
238
 
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)
239
+ [alias_scope, current_scope.where(join_constaints), klass_scope]
205
240
  end
206
-
207
- [wrapper_scope, current_scope.where(join_constaints)]
208
241
  end
209
242
 
210
- def self.constraint_allowed_lim_off_from(reflection)
243
+ def self.assoc_scope_to_keep_lim_off_from(reflection)
211
244
  # For :through associations, it's pretty hard/tricky to apply limit/offset/order of the
212
245
  # whole has_* :through. For now, we only apply those of the direct associations from one model
213
246
  # to another that the :through uses and we ignore the limit/offset/order from the scope of has_* :through.
@@ -219,13 +252,61 @@ module ActiveRecordWhereAssoc
219
252
  user_defined_actual_source_reflection(reflection).scope
220
253
  end
221
254
 
255
+ def self.classes_with_scope_for_reflection(reflection, options)
256
+ actual_source_reflection = user_defined_actual_source_reflection(reflection)
257
+
258
+ if poly_belongs_to?(actual_source_reflection)
259
+ on_poly_belongs_to = option_value(options, :poly_belongs_to)
260
+
261
+ if reflection.options[:source_type]
262
+ [reflection.options[:source_type].safe_constantize].compact
263
+ else
264
+ case on_poly_belongs_to
265
+ when :pluck
266
+ class_names = actual_source_reflection.active_record.distinct.pluck(actual_source_reflection.foreign_type)
267
+ class_names.compact.map!(&:safe_constantize).compact
268
+ when Array, Hash
269
+ array = on_poly_belongs_to.to_a
270
+ bad_class = array.detect { |c, _p| !c.is_a?(Class) || !(c < ActiveRecord::Base) }
271
+ if bad_class.is_a?(ActiveRecord::Base)
272
+ raise ArgumentError, "Must receive the Class of the model, not an instance. This is wrong: #{bad_class.inspect}"
273
+ elsif bad_class
274
+ raise ArgumentError, "Expected #{bad_class.inspect} to be a subclass of ActiveRecord::Base"
275
+ end
276
+ array
277
+ when :raise
278
+ msg = String.new
279
+ if actual_source_reflection == reflection
280
+ msg << "Association #{reflection.name.inspect} is a polymorphic belongs_to. "
281
+ else
282
+ msg << "Association #{reflection.name.inspect} is a :through relation that uses a polymorphic belongs_to"
283
+ msg << "#{actual_source_reflection.name.inspect} as source without without a source_type. "
284
+ end
285
+ msg << "This is not supported by ActiveRecord when doing joins, but it is by WhereAssoc. However, "
286
+ msg << "you must pass the :poly_belongs_to option to specify what to do in this case.\n"
287
+ msg << "See https://github.com/MaxLap/activerecord_where_assoc#poly_belongs_to"
288
+ raise ActiveRecordWhereAssoc::PolymorphicBelongsToWithoutClasses, msg
289
+ else
290
+ if on_poly_belongs_to.is_a?(Class) && on_poly_belongs_to < ActiveRecord::Base
291
+ [on_poly_belongs_to]
292
+ else
293
+ raise ArgumentError, "Received a bad value for :poly_belongs_to: #{on_poly_belongs_to.inspect}"
294
+ end
295
+ end
296
+ end
297
+ else
298
+ [reflection.klass]
299
+ end
300
+ end
301
+
302
+ # Creates a sub_query that the current_scope gets nested into if there is limit/offset to apply
222
303
  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
304
+ if user_defined_actual_source_reflection(reflection).macro == :belongs_to || option_value(options, :ignore_limit)
305
+ return current_scope.unscope(:limit, :offset, :order)
306
+ end
224
307
 
225
308
  current_scope = current_scope.limit(1) if reflection.macro == :has_one
226
309
 
227
- current_scope = current_scope.unscope(:limit, :offset) if option_value(options, :ignore_limit)
228
-
229
310
  # Order is useless without either limit or offset
230
311
  current_scope = current_scope.unscope(:order) if !current_scope.limit_value && !current_scope.offset_value
231
312
 
@@ -250,10 +331,7 @@ module ActiveRecordWhereAssoc
250
331
  # be useful.
251
332
 
252
333
  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
334
+ # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless it
257
335
  # could helps the query planner of the DB, if someone can show it to be worth it, then this can be changed.)
258
336
 
259
337
  reflection.klass.unscoped.where(reflection.klass.primary_key.to_sym => current_scope)
@@ -272,30 +350,32 @@ module ActiveRecordWhereAssoc
272
350
  # If it can receive arguments, call the proc the relation passed as argument
273
351
  def self.apply_proc_scope(relation, proc_scope)
274
352
  if proc_scope.arity == 0
275
- relation.instance_exec(&proc_scope) || relation
353
+ relation.instance_exec(nil, &proc_scope) || relation
276
354
  else
277
355
  proc_scope.call(relation) || relation
278
356
  end
279
357
  end
280
358
 
281
- def self.build_wrapper_scope_for_recursive_association(reflection)
282
- table = reflection.klass.arel_table
283
- primary_key = reflection.klass.primary_key
359
+ def self.build_alias_scope_for_recursive_association(reflection, poly_belongs_to_klass)
360
+ klass = poly_belongs_to_klass || reflection.klass
361
+ table = klass.arel_table
362
+ primary_key = klass.primary_key
284
363
  foreign_klass = reflection.send(:actual_source_reflection).active_record
285
364
 
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
365
+ alias_scope = foreign_klass.base_class.unscoped
366
+ alias_scope = alias_scope.from("#{table.name} #{ALIAS_TABLE.name}")
367
+ alias_scope = alias_scope.where(table[primary_key].eq(ALIAS_TABLE[primary_key]))
368
+ alias_scope
290
369
  end
291
370
 
292
371
  def self.wrapper_and_join_constraints(reflection, options = {})
293
- join_keys = ActiveRecordCompat.join_keys(reflection)
372
+ poly_belongs_to_klass = options[:poly_belongs_to_klass]
373
+ join_keys = ActiveRecordCompat.join_keys(reflection, poly_belongs_to_klass)
294
374
 
295
375
  key = join_keys.key
296
376
  foreign_key = join_keys.foreign_key
297
377
 
298
- table = reflection.klass.arel_table
378
+ table = (poly_belongs_to_klass || reflection.klass).arel_table
299
379
  foreign_klass = reflection.send(:actual_source_reflection).active_record
300
380
  foreign_table = foreign_klass.arel_table
301
381
 
@@ -303,25 +383,36 @@ module ActiveRecordWhereAssoc
303
383
  habtm_other_table = habtm_other_reflection.klass.arel_table if habtm_other_reflection
304
384
 
305
385
  if (habtm_other_table || table).name == foreign_table.name
306
- wrapper_scope = build_wrapper_scope_for_recursive_association(habtm_other_reflection || reflection)
386
+ alias_scope = build_alias_scope_for_recursive_association(habtm_other_reflection || reflection, poly_belongs_to_klass)
307
387
  foreign_table = ALIAS_TABLE
308
388
  end
309
389
 
310
390
  constraints = table[key].eq(foreign_table[foreign_key])
311
391
 
312
392
  if reflection.type
313
- # Handing of the polymorphic has_many/has_one's type column
393
+ # Handling of the polymorphic has_many/has_one's type column
314
394
  constraints = constraints.and(table[reflection.type].eq(foreign_klass.base_class.name))
315
395
  end
316
396
 
317
- [wrapper_scope, constraints]
397
+ if poly_belongs_to_klass
398
+ constraints = constraints.and(foreign_table[reflection.foreign_type].eq(poly_belongs_to_klass.base_class.name))
399
+ end
400
+
401
+ [alias_scope, constraints]
318
402
  end
319
403
 
404
+ # Because we work using Model._reflections, we don't actually get the :has_and_belongs_to_many.
405
+ # Instead, we get a has_many :through, which is was ActiveRecord created behind the scene.
406
+ # This code detects that a :through is actually a has_and_belongs_to_many.
320
407
  def self.has_and_belongs_to_many?(reflection) # rubocop:disable Naming/PredicateName
321
408
  parent = ActiveRecordCompat.parent_reflection(reflection)
322
409
  parent && parent.macro == :has_and_belongs_to_many
323
410
  end
324
411
 
412
+ def self.poly_belongs_to?(reflection)
413
+ reflection.macro == :belongs_to && reflection.options[:polymorphic]
414
+ end
415
+
325
416
  # Return true if #user_defined_actual_source_reflection is a has_and_belongs_to_many
326
417
  def self.actually_has_and_belongs_to_many?(reflection)
327
418
  has_and_belongs_to_many?(user_defined_actual_source_reflection(reflection))
@@ -339,6 +430,26 @@ module ActiveRecordWhereAssoc
339
430
  end
340
431
  end
341
432
 
433
+ def self.check_reflection_validity!(reflection)
434
+ if ActiveRecordCompat.through_reflection?(reflection)
435
+ # Copied from ActiveRecord
436
+ if reflection.through_reflection.polymorphic?
437
+ # Since deep_cover/builtin_takeover lacks some granularity,
438
+ # it can sometimes happen that it won't display 100% coverage while a regular would
439
+ # be 100%. This happens when multiple banches are on in a single line.
440
+ # For this reason, I split this condition in 2
441
+ if ActiveRecord.const_defined?(:HasOneAssociationPolymorphicThroughError)
442
+ if reflection.has_one?
443
+ raise ActiveRecord::HasOneAssociationPolymorphicThroughError.new(reflection.active_record.name, reflection)
444
+ end
445
+ end
446
+ raise ActiveRecord::HasManyThroughAssociationPolymorphicThroughError.new(reflection.active_record.name, reflection)
447
+ end
448
+ check_reflection_validity!(reflection.through_reflection)
449
+ check_reflection_validity!(reflection.source_reflection)
450
+ end
451
+ end
452
+
342
453
  # Doing (SQL) BETWEEN v1 AND v2, where v2 is infinite means (SQL) >= v1. However,
343
454
  # we place the SQL on the right side, so the operator is flipped to become v1 <= (SQL).
344
455
  # Doing (SQL) NOT BETWEEN v1 AND v2 where v2 is infinite means (SQL) < v1. However,
@@ -368,11 +479,12 @@ module ActiveRecordWhereAssoc
368
479
  v1 = left_operand.begin
369
480
  v2 = left_operand.end || Float::INFINITY
370
481
 
482
+ # We are doing a count and summing them, the lowest possible is 0, so just use that instead of changing the SQL used.
371
483
  v1 = 0 if v1 == -Float::INFINITY
372
484
 
373
485
  return sql_for_count_operator(v1, RIGHT_INFINITE_RANGE_OPERATOR_MAP.fetch(operator), right_sql) if v2 == Float::INFINITY
374
486
 
375
- # Its int or a float with no mantissa, exclude_end? means -1
487
+ # Its int or a rounded float. Since we are comparing to integer values (count), exclude_end? just means -1
376
488
  v2 -= 1 if left_operand.exclude_end? && v2 % 1 == 0
377
489
 
378
490
  "#{right_sql} #{RANGE_OPERATOR_MAP.fetch(operator)} #{v1} AND #{v2}"