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.
- checksums.yaml +5 -5
- data/ALTERNATIVES_PROBLEMS.md +15 -5
- data/CHANGELOG.md +6 -0
- data/EXAMPLES.md +33 -2
- data/README.md +62 -122
- data/lib/active_record_where_assoc.rb +10 -1
- data/lib/active_record_where_assoc/active_record_compat.rb +31 -9
- data/lib/active_record_where_assoc/core_logic.rb +221 -109
- data/lib/active_record_where_assoc/exceptions.rb +3 -0
- data/lib/active_record_where_assoc/query_methods.rb +315 -116
- data/lib/active_record_where_assoc/version.rb +1 -1
- metadata +5 -5
@@ -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,
|
15
|
-
|
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,
|
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((#{
|
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,
|
47
|
-
|
48
|
-
NestWithExistsBlock.call(base_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,
|
56
|
-
|
57
|
-
NestWithExistsBlock.call(base_relation,
|
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,
|
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
|
-
|
75
|
+
nested_relations = relations_on_association(base_relation, association_name, given_conditions, options, deepest_scope_mod, NestWithSumBlock)
|
72
76
|
|
73
|
-
|
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
|
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.
|
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 =
|
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
|
-
|
101
|
+
relations_on_one_association(base_relation, association_names_path.first, nil, options, recursive_scope_block, nest_assocs_block)
|
90
102
|
else
|
91
|
-
|
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
|
96
|
-
|
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
|
-
|
101
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
160
|
-
|
161
|
-
if
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
#
|
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.
|
282
|
-
|
283
|
-
|
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
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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}"
|