activerecord_where_assoc 1.1.2 → 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/EXAMPLES.md +4 -3
- data/README.md +16 -4
- data/lib/active_record_where_assoc/active_record_compat.rb +10 -0
- data/lib/active_record_where_assoc/core_logic.rb +30 -16
- data/lib/active_record_where_assoc/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d915a4b0f214283acc2a6ba098018b17fc786f2b801eb2703eba31448b127b9
|
4
|
+
data.tar.gz: 614b3c59444a0f6676b63f2499718b16f95ac1d1b010e18e322b10d25be21e1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1baf41c6ae6094045794f1c5a01c4ff3ab1218d9bc027b51e038667a76d210aa7c8819a1eb61404032fb906233400a9da2f7112a94e0610e5b546c1ef7373934
|
7
|
+
data.tar.gz: a1fbc573c244b8ea12cf8bd96b0de0ab248bc14ef5eeb5f1503715951e746ccae9e103fa98baee2f666c971496732bd4f938ece8d77cea6770ad1614d3feed43
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 1.1.4 - 2023-10-10
|
4
|
+
|
5
|
+
* Add compatibility for Rails 7.1
|
6
|
+
|
7
|
+
# 1.1.3 - 2022-08-16
|
8
|
+
|
9
|
+
* Add support for associations defined on abstract models
|
10
|
+
|
11
|
+
# 1.1.2 - 2020-12-24
|
12
|
+
|
13
|
+
* Add compatibility for Rails 6.1
|
14
|
+
|
3
15
|
# 1.1.1 - 2020-04-13
|
4
16
|
|
5
17
|
* Fix handling for ActiveRecord's NullRelation (MyModel.none) in block and association's conditions.
|
data/EXAMPLES.md
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
SELECT "users".* FROM "users"
|
1
2
|
Here are some example usages of the gem, along with the generated SQL.
|
2
3
|
|
3
4
|
Each of those methods can be chained with scoping methods, so they can be used on `Post`, `my_user.posts`, `Post.where('hello')` or inside a scope. Note that for the `*_sql` variants, those should preferably be used on classes only, because otherwise, it could be confusing for a reader.
|
@@ -107,13 +108,13 @@ User.where_assoc_exists(:posts).or(User.where_assoc_exists(:comments))
|
|
107
108
|
```
|
108
109
|
```sql
|
109
110
|
SELECT "users".* FROM "users"
|
110
|
-
WHERE (
|
111
|
+
WHERE (EXISTS (
|
111
112
|
SELECT 1 FROM "posts"
|
112
113
|
WHERE "posts"."author_id" = "users"."id"
|
113
|
-
)
|
114
|
+
) OR EXISTS (
|
114
115
|
SELECT 1 FROM "comments"
|
115
116
|
WHERE "comments"."author_id" = "users"."id"
|
116
|
-
))
|
117
|
+
))
|
117
118
|
```
|
118
119
|
|
119
120
|
---
|
data/README.md
CHANGED
@@ -37,9 +37,8 @@ These methods have many advantages over the alternative ways of achieving the si
|
|
37
37
|
|
38
38
|
## Installation
|
39
39
|
|
40
|
-
Rails 4.1 to
|
41
|
-
|
42
|
-
The gem only depends on the `activerecord` gem.
|
40
|
+
Rails 4.1 to 7.0 are supported with Ruby 2.1 to 3.1. Tested against SQLite3, PostgreSQL and MySQL. The gem
|
41
|
+
only depends on the `activerecord` gem.
|
43
42
|
|
44
43
|
Add this line to your application's Gemfile:
|
45
44
|
|
@@ -51,7 +50,7 @@ And then execute:
|
|
51
50
|
|
52
51
|
$ bundle install
|
53
52
|
|
54
|
-
Or install it yourself
|
53
|
+
Or install it yourself with:
|
55
54
|
|
56
55
|
$ gem install activerecord_where_assoc
|
57
56
|
|
@@ -235,6 +234,19 @@ It is pretty complicated to support `#limit` and `#offset` of the `has_* :throug
|
|
235
234
|
|
236
235
|
Note that the support of `#limit` and `#offset` for the `:source` and `:through` parts is a feature. I consider `ActiveRecord` wrong for not handling them correctly.
|
237
236
|
|
237
|
+
## Another recommended gem
|
238
|
+
|
239
|
+
If you feel a need for this gem's feature, you may also be interested in another gem I made: [activerecord_follow_assoc](https://github.com/MaxLap/activerecord_follow_assoc).
|
240
|
+
|
241
|
+
It allows you to follow an association of your choice while building a query (a scope). You start querying posts, and then you change to querying the authors of those posts. For simple cases, it's possible that both `where_assoc` and `follow_assoc` can build the query your need, but each can handle different situations. Here is an example:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
# Find every posts that have comments by an admin
|
245
|
+
Post.where_assoc_exists([:comments, :author], &:admins)
|
246
|
+
```
|
247
|
+
|
248
|
+
This could be done with `follow_assoc`: `User.admins.follow_assoc(:comments, :post)`. But if you wanted conditions on a second association, then `follow_assoc` wouldn't work. On the other hand, if you received a scope on users and wanted their posts, then `follow_assoc` would be a nicer tool for the job. It all depends on the context where you need to do the query and what starting point you have.
|
249
|
+
|
238
250
|
## Development
|
239
251
|
|
240
252
|
After checking out the repo, run `bundle install` to install dependencies.
|
@@ -91,5 +91,15 @@ module ActiveRecordWhereAssoc
|
|
91
91
|
reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
92
92
|
end
|
93
93
|
end
|
94
|
+
|
95
|
+
if ActiveRecord.gem_version >= Gem::Version.new("7.1.0.alpha")
|
96
|
+
def self.null_relation?(reflection)
|
97
|
+
reflection.null_relation?
|
98
|
+
end
|
99
|
+
else
|
100
|
+
def self.null_relation?(reflection)
|
101
|
+
reflection.is_a?(ActiveRecord::NullRelation)
|
102
|
+
end
|
103
|
+
end
|
94
104
|
end
|
95
105
|
end
|
@@ -13,7 +13,7 @@ module ActiveRecordWhereAssoc
|
|
13
13
|
# => "EXISTS (SELECT... *relation1*) OR EXISTS (SELECT... *relation2*)"
|
14
14
|
def self.sql_for_any_exists(relations)
|
15
15
|
relations = [relations] unless relations.is_a?(Array)
|
16
|
-
relations = relations.reject { |rel|
|
16
|
+
relations = relations.reject { |rel| ActiveRecordCompat.null_relation?(rel) }
|
17
17
|
sqls = relations.map { |rel| "EXISTS (#{rel.select('1').to_sql})" }
|
18
18
|
if sqls.size > 1
|
19
19
|
"(#{sqls.join(" OR ")})" # Parens needed when embedding the sql in a `where`, because the OR could make things wrong
|
@@ -33,7 +33,7 @@ module ActiveRecordWhereAssoc
|
|
33
33
|
# => "SUM((SELECT... *relation1*)) + SUM((SELECT... *relation2*))"
|
34
34
|
def self.sql_for_sum_of_counts(relations)
|
35
35
|
relations = [relations] unless relations.is_a?(Array)
|
36
|
-
relations = relations.reject { |rel|
|
36
|
+
relations = relations.reject { |rel| ActiveRecordCompat.null_relation?(rel) }
|
37
37
|
# Need the double parentheses
|
38
38
|
relations.map { |rel| "SUM((#{rel.to_sql}))" }.join(" + ").presence || "0"
|
39
39
|
end
|
@@ -85,7 +85,7 @@ module ActiveRecordWhereAssoc
|
|
85
85
|
end
|
86
86
|
|
87
87
|
nested_relations = relations_on_association(record_class, association_names, given_conditions, options, deepest_scope_mod, NestWithSumBlock)
|
88
|
-
nested_relations = nested_relations.reject { |rel|
|
88
|
+
nested_relations = nested_relations.reject { |rel| ActiveRecordCompat.null_relation?(rel) }
|
89
89
|
nested_relations.map { |nr| "COALESCE((#{nr.to_sql}), 0)" }.join(" + ").presence || "0"
|
90
90
|
end
|
91
91
|
|
@@ -141,7 +141,7 @@ module ActiveRecordWhereAssoc
|
|
141
141
|
# Each step, we get all of the scoping lambdas that were defined on associations that apply for
|
142
142
|
# the reflection's target
|
143
143
|
# Basically, we start from the deepest part of the query and wrap it up
|
144
|
-
reflection_chain,
|
144
|
+
reflection_chain, constraints_chain = ActiveRecordCompat.chained_reflection_and_chained_constraints(final_reflection)
|
145
145
|
skip_next = false
|
146
146
|
|
147
147
|
reflection_chain.each_with_index do |reflection, i|
|
@@ -153,7 +153,7 @@ module ActiveRecordWhereAssoc
|
|
153
153
|
# the 2nd part of has_and_belongs_to_many is handled at the same time as the first.
|
154
154
|
skip_next = true if actually_has_and_belongs_to_many?(reflection)
|
155
155
|
|
156
|
-
init_scopes = initial_scopes_from_reflection(reflection_chain[i..-1],
|
156
|
+
init_scopes = initial_scopes_from_reflection(record_class, reflection_chain[i..-1], constraints_chain[i], options)
|
157
157
|
current_scopes = init_scopes.map do |alias_scope, current_scope, klass_scope|
|
158
158
|
current_scope = process_association_step_limits(current_scope, reflection, record_class, options)
|
159
159
|
|
@@ -197,13 +197,13 @@ module ActiveRecordWhereAssoc
|
|
197
197
|
end
|
198
198
|
|
199
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)
|
200
|
+
def self.initial_scopes_from_reflection(record_class, reflection_chain, assoc_scopes, options)
|
201
201
|
reflection = reflection_chain.first
|
202
202
|
actual_source_reflection = user_defined_actual_source_reflection(reflection)
|
203
203
|
|
204
204
|
on_poly_belongs_to = option_value(options, :poly_belongs_to) if poly_belongs_to?(actual_source_reflection)
|
205
205
|
|
206
|
-
classes_with_scope = classes_with_scope_for_reflection(reflection, options)
|
206
|
+
classes_with_scope = classes_with_scope_for_reflection(record_class, reflection, options)
|
207
207
|
|
208
208
|
assoc_scope_allowed_lim_off = assoc_scope_to_keep_lim_off_from(reflection)
|
209
209
|
|
@@ -225,18 +225,18 @@ module ActiveRecordWhereAssoc
|
|
225
225
|
# would be great, except we cannot add a given_conditions afterward because we are on the wrong "base class",
|
226
226
|
# and we can't do #merge because of the LEW crap.
|
227
227
|
# So we must do the joins ourself!
|
228
|
-
_wrapper, sub_join_contraints = wrapper_and_join_constraints(reflection)
|
228
|
+
_wrapper, sub_join_contraints = wrapper_and_join_constraints(record_class, reflection)
|
229
229
|
next_reflection = reflection_chain[1]
|
230
230
|
|
231
231
|
current_scope = current_scope.joins(<<-SQL)
|
232
232
|
INNER JOIN #{next_reflection.klass.quoted_table_name} ON #{sub_join_contraints.to_sql}
|
233
233
|
SQL
|
234
234
|
|
235
|
-
alias_scope,
|
235
|
+
alias_scope, join_constraints = wrapper_and_join_constraints(record_class, next_reflection, habtm_other_reflection: reflection)
|
236
236
|
elsif on_poly_belongs_to
|
237
|
-
alias_scope,
|
237
|
+
alias_scope, join_constraints = wrapper_and_join_constraints(record_class, reflection, poly_belongs_to_klass: klass)
|
238
238
|
else
|
239
|
-
alias_scope,
|
239
|
+
alias_scope, join_constraints = wrapper_and_join_constraints(record_class, reflection)
|
240
240
|
end
|
241
241
|
|
242
242
|
assoc_scopes.each do |callable|
|
@@ -256,7 +256,7 @@ module ActiveRecordWhereAssoc
|
|
256
256
|
current_scope = current_scope.merge(relation)
|
257
257
|
end
|
258
258
|
|
259
|
-
[alias_scope, current_scope.where(
|
259
|
+
[alias_scope, current_scope.where(join_constraints), klass_scope]
|
260
260
|
end
|
261
261
|
end
|
262
262
|
|
@@ -272,7 +272,7 @@ module ActiveRecordWhereAssoc
|
|
272
272
|
user_defined_actual_source_reflection(reflection).scope
|
273
273
|
end
|
274
274
|
|
275
|
-
def self.classes_with_scope_for_reflection(reflection, options)
|
275
|
+
def self.classes_with_scope_for_reflection(record_class, reflection, options)
|
276
276
|
actual_source_reflection = user_defined_actual_source_reflection(reflection)
|
277
277
|
|
278
278
|
if poly_belongs_to?(actual_source_reflection)
|
@@ -283,7 +283,15 @@ module ActiveRecordWhereAssoc
|
|
283
283
|
else
|
284
284
|
case on_poly_belongs_to
|
285
285
|
when :pluck
|
286
|
-
|
286
|
+
model_for_ids = actual_source_reflection.active_record
|
287
|
+
|
288
|
+
if model_for_ids.abstract_class
|
289
|
+
# When the reflection is defined on an abstract model, we fallback to the model
|
290
|
+
# on which this was called
|
291
|
+
model_for_ids = record_class
|
292
|
+
end
|
293
|
+
|
294
|
+
class_names = model_for_ids.distinct.pluck(actual_source_reflection.foreign_type)
|
287
295
|
class_names.compact.map!(&:safe_constantize).compact
|
288
296
|
when Array, Hash
|
289
297
|
array = on_poly_belongs_to.to_a
|
@@ -326,7 +334,7 @@ module ActiveRecordWhereAssoc
|
|
326
334
|
end
|
327
335
|
|
328
336
|
# No need to do transformations if this is already a NullRelation
|
329
|
-
return current_scope if
|
337
|
+
return current_scope if ActiveRecordCompat.null_relation?(current_scope)
|
330
338
|
|
331
339
|
current_scope = current_scope.limit(1) if reflection.macro == :has_one
|
332
340
|
|
@@ -391,7 +399,7 @@ module ActiveRecordWhereAssoc
|
|
391
399
|
alias_scope
|
392
400
|
end
|
393
401
|
|
394
|
-
def self.wrapper_and_join_constraints(reflection, options = {})
|
402
|
+
def self.wrapper_and_join_constraints(record_class, reflection, options = {})
|
395
403
|
poly_belongs_to_klass = options[:poly_belongs_to_klass]
|
396
404
|
join_keys = ActiveRecordCompat.join_keys(reflection, poly_belongs_to_klass)
|
397
405
|
|
@@ -400,6 +408,12 @@ module ActiveRecordWhereAssoc
|
|
400
408
|
|
401
409
|
table = (poly_belongs_to_klass || reflection.klass).arel_table
|
402
410
|
foreign_klass = reflection.send(:actual_source_reflection).active_record
|
411
|
+
if foreign_klass.abstract_class
|
412
|
+
# When the reflection is defined on an abstract model, we fallback to the model
|
413
|
+
# on which this was called
|
414
|
+
foreign_klass = record_class
|
415
|
+
end
|
416
|
+
|
403
417
|
foreign_table = foreign_klass.arel_table
|
404
418
|
|
405
419
|
habtm_other_reflection = options[:habtm_other_reflection]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord_where_assoc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maxime Handfield Lapointe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -31,6 +31,9 @@ dependencies:
|
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.15'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '2.6'
|
34
37
|
type: :development
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -38,6 +41,9 @@ dependencies:
|
|
38
41
|
- - ">="
|
39
42
|
- !ruby/object:Gem::Version
|
40
43
|
version: '1.15'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.6'
|
41
47
|
- !ruby/object:Gem::Dependency
|
42
48
|
name: minitest
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
190
196
|
- !ruby/object:Gem::Version
|
191
197
|
version: '0'
|
192
198
|
requirements: []
|
193
|
-
rubygems_version: 3.
|
199
|
+
rubygems_version: 3.4.10
|
194
200
|
signing_key:
|
195
201
|
specification_version: 4
|
196
202
|
summary: Make ActiveRecord do conditions on your associations
|