activerecord_where_assoc 0.1.3 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/EXAMPLES.md +150 -67
- data/README.md +107 -129
- data/lib/active_record_where_assoc.rb +17 -6
- data/lib/active_record_where_assoc/active_record_compat.rb +42 -10
- data/lib/active_record_where_assoc/core_logic.rb +265 -130
- data/lib/active_record_where_assoc/exceptions.rb +3 -0
- data/lib/active_record_where_assoc/relation_returning_delegates.rb +12 -0
- data/lib/active_record_where_assoc/relation_returning_methods.rb +408 -0
- data/lib/active_record_where_assoc/sql_returning_methods.rb +74 -0
- data/lib/active_record_where_assoc/version.rb +1 -1
- metadata +10 -25
- data/ALTERNATIVES_PROBLEMS.md +0 -221
- data/lib/active_record_where_assoc/query_methods.rb +0 -180
- data/lib/active_record_where_assoc/querying.rb +0 -11
@@ -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/
|
18
|
-
require_relative "active_record_where_assoc/
|
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
|
-
|
24
|
-
ActiveRecord::
|
25
|
-
ActiveRecord::Base.extend(ActiveRecordWhereAssoc::
|
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("
|
6
|
-
|
7
|
-
|
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
|
-
|
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
|
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
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
#
|
21
|
-
#
|
22
|
-
|
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
|
-
|
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
|
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
|
46
|
-
def self.
|
47
|
-
|
48
|
-
|
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
|
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 #
|
55
|
-
def self.
|
56
|
-
|
57
|
-
|
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
|
-
#
|
61
|
-
#
|
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 #
|
64
|
-
def self.
|
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
|
-
|
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
|
-
|
74
|
-
base_relation.where(sql)
|
98
|
+
sql_for_count_operator(left_operand, operator, right_sql)
|
75
99
|
end
|
76
100
|
|
77
|
-
# Returns
|
78
|
-
#
|
79
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
122
|
+
relations_on_one_association(record_class, association_names.first, nil, options, recursive_scope_block, nest_assocs_block)
|
90
123
|
else
|
91
|
-
|
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
|
96
|
-
|
97
|
-
|
98
|
-
final_reflection = fetch_reflection(
|
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
|
-
|
101
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
#
|
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.
|
282
|
-
|
283
|
-
|
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
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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}"
|