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