activerecord_where_assoc 0.1.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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}"