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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c33267f5201fd0465cceec484dd0a2c91b3511b47289f500311e0d3a3946b28f
|
4
|
+
data.tar.gz: 0b1ef81c3b3106a0226ed72494523ce3a2ebb005f4b1401d6cca3917a78bfa26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 156df4226e93a9d5a8e900a71b518d42ff984a47a8917d7d807429d3b473a5f8db7d392496c8f4dfda7f0746689b9127547d238b2f875846235c9075e4789f7b
|
7
|
+
data.tar.gz: fbea2c6d875f87c5b04d740fb6878d1f9ece9e11ba0053d38bbe6c8cb6311b194be770ebd402bc59a2baad10c3ecac8ee9f5e2bad8f6147ea152d8e8c2a96518
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
# Unreleased
|
2
|
+
|
3
|
+
# 1.1.1 - 2020-04-13
|
4
|
+
|
5
|
+
* Fix handling for ActiveRecord's NullRelation (MyModel.none) in block and association's conditions.
|
6
|
+
|
7
|
+
# 1.1.0 - 2020-02-24
|
8
|
+
|
9
|
+
* Added methods which return the SQL used by this gem: `assoc_exists_sql`, `assoc_not_exists_sql`, `compare_assoc_count_sql`, `only_assoc_count_sql`
|
10
|
+
[Documentation for them](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/SqlReturningMethods.html)
|
11
|
+
|
12
|
+
# 1.0.1
|
13
|
+
|
14
|
+
* Fix broken urls in error messages
|
15
|
+
|
16
|
+
# 1.0.0
|
17
|
+
|
18
|
+
* Now supports polymorphic belongs_to
|
19
|
+
|
20
|
+
# 0.1.3
|
1
21
|
|
2
22
|
* Use `SELECT 1` instead of `SELECT 0`...
|
3
23
|
... it just seems more natural that way.
|
data/EXAMPLES.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
Here are some example usages of the gem, along with the generated SQL.
|
1
|
+
Here are some example usages of the gem, along with the generated SQL.
|
2
|
+
|
3
|
+
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.
|
2
4
|
|
3
5
|
The models can be found in [examples/models.md](examples/models.md). The comments in that file explain how to get a console to try the queries. There are also example uses of the gem for scopes.
|
4
6
|
|
5
|
-
The content of this file is generated
|
7
|
+
The content of this file is generated when running `rake`
|
6
8
|
|
7
9
|
-------
|
8
10
|
|
@@ -14,10 +16,10 @@ Post.where_assoc_exists(:comments)
|
|
14
16
|
```
|
15
17
|
```sql
|
16
18
|
SELECT "posts".* FROM "posts"
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
WHERE (EXISTS (
|
20
|
+
SELECT 1 FROM "comments"
|
21
|
+
WHERE "comments"."post_id" = "posts"."id"
|
22
|
+
))
|
21
23
|
```
|
22
24
|
|
23
25
|
---
|
@@ -28,10 +30,10 @@ Post.where_assoc_not_exists(:comments)
|
|
28
30
|
```
|
29
31
|
```sql
|
30
32
|
SELECT "posts".* FROM "posts"
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
WHERE (NOT EXISTS (
|
34
|
+
SELECT 1 FROM "comments"
|
35
|
+
WHERE "comments"."post_id" = "posts"."id"
|
36
|
+
))
|
35
37
|
```
|
36
38
|
|
37
39
|
---
|
@@ -42,10 +44,76 @@ Post.where_assoc_count(50, :<=, :comments)
|
|
42
44
|
```
|
43
45
|
```sql
|
44
46
|
SELECT "posts".* FROM "posts"
|
45
|
-
|
46
|
-
|
47
|
+
WHERE ((50) <= COALESCE((
|
48
|
+
SELECT COUNT(*) FROM "comments"
|
49
|
+
WHERE "comments"."post_id" = "posts"."id"
|
50
|
+
), 0))
|
51
|
+
```
|
52
|
+
|
53
|
+
---
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# Users that have made posts
|
57
|
+
User.where_assoc_exists(:posts)
|
58
|
+
```
|
59
|
+
```sql
|
60
|
+
SELECT "users".* FROM "users"
|
61
|
+
WHERE (EXISTS (
|
62
|
+
SELECT 1 FROM "posts"
|
63
|
+
WHERE "posts"."author_id" = "users"."id"
|
64
|
+
))
|
65
|
+
```
|
66
|
+
|
67
|
+
---
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# Users that have made posts that have comments
|
71
|
+
User.where_assoc_exists([:posts, :comments])
|
72
|
+
```
|
73
|
+
```sql
|
74
|
+
SELECT "users".* FROM "users"
|
75
|
+
WHERE (EXISTS (
|
76
|
+
SELECT 1 FROM "posts"
|
77
|
+
WHERE "posts"."author_id" = "users"."id" AND (EXISTS (
|
78
|
+
SELECT 1 FROM "comments"
|
47
79
|
WHERE "comments"."post_id" = "posts"."id"
|
48
|
-
)
|
80
|
+
))
|
81
|
+
))
|
82
|
+
```
|
83
|
+
|
84
|
+
---
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Users with a post or a comment (without using ActiveRecord's `or` method)
|
88
|
+
# Using `my_users` to highlight that *_sql methods should always be called on the class
|
89
|
+
my_users.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}")
|
90
|
+
```
|
91
|
+
```sql
|
92
|
+
SELECT "users".* FROM "users"
|
93
|
+
WHERE (EXISTS (
|
94
|
+
SELECT 1 FROM "posts"
|
95
|
+
WHERE "posts"."author_id" = "users"."id"
|
96
|
+
) OR EXISTS (
|
97
|
+
SELECT 1 FROM "comments"
|
98
|
+
WHERE "comments"."author_id" = "users"."id"
|
99
|
+
))
|
100
|
+
```
|
101
|
+
|
102
|
+
---
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# Users with a post or a comment (using ActiveRecord's `or` method)
|
106
|
+
User.where_assoc_exists(:posts).or(User.where_assoc_exists(:comments))
|
107
|
+
```
|
108
|
+
```sql
|
109
|
+
SELECT "users".* FROM "users"
|
110
|
+
WHERE ((EXISTS (
|
111
|
+
SELECT 1 FROM "posts"
|
112
|
+
WHERE "posts"."author_id" = "users"."id"
|
113
|
+
)) OR (EXISTS (
|
114
|
+
SELECT 1 FROM "comments"
|
115
|
+
WHERE "comments"."author_id" = "users"."id"
|
116
|
+
)))
|
49
117
|
```
|
50
118
|
|
51
119
|
---
|
@@ -58,10 +126,10 @@ my_post.comments.where_assoc_exists(:author, is_admin: true)
|
|
58
126
|
```
|
59
127
|
```sql
|
60
128
|
SELECT "comments".* FROM "comments"
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
129
|
+
WHERE "comments"."post_id" = 1 AND (EXISTS (
|
130
|
+
SELECT 1 FROM "users"
|
131
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1
|
132
|
+
))
|
65
133
|
```
|
66
134
|
|
67
135
|
---
|
@@ -72,10 +140,10 @@ my_post.comments.where_assoc_not_exists(:author, &:admins)
|
|
72
140
|
```
|
73
141
|
```sql
|
74
142
|
SELECT "comments".* FROM "comments"
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
143
|
+
WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
|
144
|
+
SELECT 1 FROM "users"
|
145
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1
|
146
|
+
))
|
79
147
|
```
|
80
148
|
|
81
149
|
---
|
@@ -86,10 +154,10 @@ Post.where_assoc_count(5, :<=, :comments, ["is_reported = ?", true])
|
|
86
154
|
```
|
87
155
|
```sql
|
88
156
|
SELECT "posts".* FROM "posts"
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
157
|
+
WHERE ((5) <= COALESCE((
|
158
|
+
SELECT COUNT(*) FROM "comments"
|
159
|
+
WHERE "comments"."post_id" = "posts"."id" AND (is_reported = 1)
|
160
|
+
), 0))
|
93
161
|
```
|
94
162
|
|
95
163
|
---
|
@@ -100,10 +168,10 @@ Post.where_assoc_exists(:author, "is_admin = 't'")
|
|
100
168
|
```
|
101
169
|
```sql
|
102
170
|
SELECT "posts".* FROM "posts"
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
171
|
+
WHERE (EXISTS (
|
172
|
+
SELECT 1 FROM "users"
|
173
|
+
WHERE "users"."id" = "posts"."author_id" AND (is_admin = 't')
|
174
|
+
))
|
107
175
|
```
|
108
176
|
|
109
177
|
---
|
@@ -114,10 +182,10 @@ my_post.comments.where_assoc_not_exists(:author) { admins }
|
|
114
182
|
```
|
115
183
|
```sql
|
116
184
|
SELECT "comments".* FROM "comments"
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
185
|
+
WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
|
186
|
+
SELECT 1 FROM "users"
|
187
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1
|
188
|
+
))
|
121
189
|
```
|
122
190
|
|
123
191
|
---
|
@@ -128,10 +196,10 @@ Post.where_assoc_count(5..10, :==, :comments) { where(is_reported: true) }
|
|
128
196
|
```
|
129
197
|
```sql
|
130
198
|
SELECT "posts".* FROM "posts"
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
199
|
+
WHERE (COALESCE((
|
200
|
+
SELECT COUNT(*) FROM "comments"
|
201
|
+
WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 1
|
202
|
+
), 0) BETWEEN 5 AND 10)
|
135
203
|
```
|
136
204
|
|
137
205
|
---
|
@@ -142,10 +210,10 @@ Comment.where_assoc_exists(:post, author_id: my_user.id)
|
|
142
210
|
```
|
143
211
|
```sql
|
144
212
|
SELECT "comments".* FROM "comments"
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
213
|
+
WHERE (EXISTS (
|
214
|
+
SELECT 1 FROM "posts"
|
215
|
+
WHERE "posts"."id" = "comments"."post_id" AND "posts"."author_id" = 1
|
216
|
+
))
|
149
217
|
```
|
150
218
|
|
151
219
|
---
|
@@ -158,27 +226,27 @@ Post.where_assoc_exists([:comments, :author], is_admin: true)
|
|
158
226
|
```
|
159
227
|
```sql
|
160
228
|
SELECT "posts".* FROM "posts"
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
))
|
229
|
+
WHERE (EXISTS (
|
230
|
+
SELECT 1 FROM "comments"
|
231
|
+
WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
|
232
|
+
SELECT 1 FROM "users"
|
233
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1
|
167
234
|
))
|
235
|
+
))
|
168
236
|
```
|
169
237
|
|
170
238
|
---
|
171
239
|
|
172
240
|
```ruby
|
173
|
-
# posts where the author also commented on the post (
|
241
|
+
# posts where the author also commented on the post (uses a conditions between tables)
|
174
242
|
Post.where_assoc_exists(:comments, "posts.author_id = comments.author_id")
|
175
243
|
```
|
176
244
|
```sql
|
177
245
|
SELECT "posts".* FROM "posts"
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
246
|
+
WHERE (EXISTS (
|
247
|
+
SELECT 1 FROM "comments"
|
248
|
+
WHERE "comments"."post_id" = "posts"."id" AND (posts.author_id = comments.author_id)
|
249
|
+
))
|
182
250
|
```
|
183
251
|
|
184
252
|
---
|
@@ -191,13 +259,13 @@ Post.where_assoc_exists(:comments, is_reported: true) {
|
|
191
259
|
```
|
192
260
|
```sql
|
193
261
|
SELECT "posts".* FROM "posts"
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
))
|
262
|
+
WHERE (EXISTS (
|
263
|
+
SELECT 1 FROM "comments"
|
264
|
+
WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 1 AND (EXISTS (
|
265
|
+
SELECT 1 FROM "users"
|
266
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1
|
200
267
|
))
|
268
|
+
))
|
201
269
|
```
|
202
270
|
|
203
271
|
---
|
@@ -209,14 +277,29 @@ my_user.posts.where_assoc_exists(:comments, is_reported: true)
|
|
209
277
|
```
|
210
278
|
```sql
|
211
279
|
SELECT "posts".* FROM "posts"
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
))
|
280
|
+
WHERE "posts"."author_id" = 1 AND (EXISTS (
|
281
|
+
SELECT 1 FROM "comments"
|
282
|
+
WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 1
|
283
|
+
)) AND (EXISTS (
|
284
|
+
SELECT 1 FROM "comments"
|
285
|
+
WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
|
286
|
+
SELECT 1 FROM "users"
|
287
|
+
WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1
|
221
288
|
))
|
289
|
+
))
|
290
|
+
```
|
291
|
+
```ruby
|
292
|
+
# Users with more posts than comments
|
293
|
+
# Using `my_users` to highlight that *_sql methods should always be called on the class
|
294
|
+
my_users.where("#{User.only_assoc_count_sql(:posts)} > #{User.only_assoc_count_sql(:comments)}")
|
295
|
+
```
|
296
|
+
```sql
|
297
|
+
SELECT "users".* FROM "users"
|
298
|
+
WHERE (COALESCE((
|
299
|
+
SELECT COUNT(*) FROM "posts"
|
300
|
+
WHERE "posts"."author_id" = "users"."id"
|
301
|
+
), 0) > COALESCE((
|
302
|
+
SELECT COUNT(*) FROM "comments"
|
303
|
+
WHERE "comments"."author_id" = "users"."id"
|
304
|
+
), 0))
|
222
305
|
```
|
data/README.md
CHANGED
@@ -1,50 +1,50 @@
|
|
1
1
|
# ActiveRecord Where Assoc
|
2
2
|
|
3
|
-
|
4
|
-
[![Coverage Status](https://coveralls.io/repos/github/MaxLap/activerecord_where_assoc/badge.svg)](https://coveralls.io/github/MaxLap/activerecord_where_assoc)
|
3
|
+
![Test supported versions](https://github.com/MaxLap/activerecord_where_assoc/workflows/Test%20supported%20versions/badge.svg)
|
5
4
|
[![Code Climate](https://codeclimate.com/github/MaxLap/activerecord_where_assoc/badges/gpa.svg)](https://codeclimate.com/github/MaxLap/activerecord_where_assoc)
|
6
|
-
[![Issue Count](https://codeclimate.com/github/MaxLap/activerecord_where_assoc/badges/issue_count.svg)](https://codeclimate.com/github/MaxLap/activerecord_where_assoc)
|
7
5
|
|
8
|
-
This gem
|
6
|
+
This gem makes it easy to do conditions based on the associations of your records in ActiveRecord (Rails). (Using SQL's `EXISTS` operator)
|
9
7
|
|
10
8
|
```ruby
|
11
9
|
# Find my_post's comments that were not made by an admin
|
12
10
|
my_post.comments.where_assoc_not_exists(:author, is_admin: true).where(...)
|
13
|
-
|
14
|
-
# Find posts that have comments by an admin
|
11
|
+
|
12
|
+
# Find every posts that have comments by an admin
|
15
13
|
Post.where_assoc_exists([:comments, :author], &:admins).where(...)
|
16
|
-
|
17
|
-
# Find my_user's posts that have at least 5 non-spam comments
|
18
|
-
my_user.posts.where_assoc_count(5, :>=, :comments) { |comments| comments.
|
14
|
+
|
15
|
+
# Find my_user's posts that have at least 5 non-spam comments (not_spam is a scope on comments)
|
16
|
+
my_user.posts.where_assoc_count(5, :>=, :comments) { |comments| comments.not_spam }.where(...)
|
19
17
|
```
|
20
18
|
|
21
19
|
These allow for powerful, chainable, clear and easy to reuse queries. (Great for scopes)
|
22
20
|
|
23
|
-
|
21
|
+
Here is an [introduction to this gem](INTRODUCTION.md).
|
24
22
|
|
25
|
-
|
23
|
+
You avoid many [problems with the alternative options](ALTERNATIVES_PROBLEMS.md).
|
26
24
|
|
27
25
|
Here are [many examples](EXAMPLES.md), including the generated SQL queries.
|
28
26
|
|
29
|
-
##
|
30
|
-
|
31
|
-
This gem is very new. If you have any feedback, good or bad, do not hesitate to write it here: [General feedback](https://github.com/MaxLap/activerecord_where_assoc/issues/3). If you find any bug, please create a new issue.
|
32
|
-
|
33
|
-
* Failure stories, if you had difficulties that kept you from using the gem.
|
34
|
-
* Success stories, if you are using it and things are going great, I wanna hear this too.
|
35
|
-
* Suggestions to make the documentation easier to follow / more complete.
|
36
|
-
|
37
|
-
|
38
|
-
## 0.1
|
27
|
+
## Advantages
|
39
28
|
|
40
|
-
|
29
|
+
These methods have many advantages over the alternative ways of achieving the similar results:
|
30
|
+
* Avoids the [problems with the alternative ways](ALTERNATIVES_PROBLEMS.md)
|
31
|
+
* Can be chained and nested with regular ActiveRecord methods (`where`, `merge`, `scope`, etc).
|
32
|
+
* Adds a single condition in the `WHERE` of the query instead of complex things like joins.
|
33
|
+
So it's easy to have multiple conditions on the same association
|
34
|
+
* Handles `has_one` correctly: only testing the "first" record of the association that matches the default_scope and the scope on the association itself.
|
35
|
+
* Handles recursive associations (such as parent/children) seemlessly.
|
36
|
+
* Can be used to quickly generate a SQL query that you can edit/use manually.
|
41
37
|
|
42
38
|
## Installation
|
43
39
|
|
40
|
+
Rails 4.1 to 6.1 are supported with Ruby 2.1 to 2.7.
|
41
|
+
Tested against SQLite3, PostgreSQL and MySQL.
|
42
|
+
The gem only depends on the `activerecord` gem.
|
43
|
+
|
44
44
|
Add this line to your application's Gemfile:
|
45
45
|
|
46
46
|
```ruby
|
47
|
-
gem 'activerecord_where_assoc'
|
47
|
+
gem 'activerecord_where_assoc', '~> 1.0'
|
48
48
|
```
|
49
49
|
|
50
50
|
And then execute:
|
@@ -55,114 +55,83 @@ Or install it yourself as:
|
|
55
55
|
|
56
56
|
$ gem install activerecord_where_assoc
|
57
57
|
|
58
|
-
##
|
59
|
-
|
60
|
-
### `#where_assoc_exists` & `#where_assoc_not_exists`
|
61
|
-
|
62
|
-
Returns a new relation, which is the result of filtering the current relation based on if a record for the specified association of the model exists (or not). Conditions that the associated model must match to count as existing can also be specified.
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
Post.where_assoc_exists(:comments, is_spam: true)
|
66
|
-
Post.where_assoc_not_exists(:comments, is_spam: true)
|
67
|
-
```
|
68
|
-
|
69
|
-
* 1st parameter: the association we are doing the condition on.
|
70
|
-
* 2nd parameter: (optional) the condition to apply on the association. It can be anything that `#where` can receive, so: Hash, String and Array (string with binds).
|
71
|
-
* 3rd parameter: [options (listed below)](#options) to alter some behaviors. (rarely necessary)
|
72
|
-
* block: adds more complex conditions by receiving a relation on the association. Can apply `#where`, `#where_assoc_*`, scopes, and other scoping methods.
|
73
|
-
The block either:
|
74
|
-
|
75
|
-
* receives no argument, in which case `self` is set to the relation, so you can do `{ where(id: 123) }`
|
76
|
-
* receives arguments, in which case the block is called with the relation as first parameter
|
77
|
-
|
78
|
-
The block should return the new relation to use or `nil` to do as if there were no blocks
|
79
|
-
It's common to use `where_assoc_*(..., &:scope_name)` to apply a single scope quickly
|
80
|
-
|
81
|
-
### `#where_assoc_count`
|
82
|
-
|
83
|
-
This is a generalization of `#where_assoc_exists` and `#where_assoc_not_exists`. It behaves the same way as them, but is more flexible as it allows you to be specific about how many matches there should be. To clarify, here are equivalent examples:
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
Post.where_assoc_exists(:comments, is_spam: true)
|
87
|
-
Post.where_assoc_count(1, :<=, :comments, is_spam: true)
|
88
|
-
|
89
|
-
Post.where_assoc_not_exists(:comments, is_spam: true)
|
90
|
-
Post.where_assoc_count(0, :==, :comments, is_spam: true)
|
91
|
-
```
|
92
|
-
|
93
|
-
* 1st parameter: the left side of the comparison. One of:
|
94
|
-
* a number
|
95
|
-
* a string of SQL to embed in the query
|
96
|
-
* a range (operator must be `:==` or `:!=`)
|
97
|
-
will use SQL's `BETWEEN` or `NOT BETWEEN`
|
98
|
-
supports infinite ranges and exclusive end
|
99
|
-
* 2nd parameter: the operator to use: `:<`, `:<=`, `:==`, `:!=`, `:>=`, `:>`
|
100
|
-
* 3rd, 4th, 5th parameters are the same as the 1st, 2nd and 3rd parameters of `#where_assoc_exists`.
|
101
|
-
* block: same as `#where_assoc_exists`' block
|
58
|
+
## Development state
|
102
59
|
|
103
|
-
|
60
|
+
This gem is feature complete and production ready.
|
61
|
+
Other than rare tweaks as new versions of Rails and Ruby are released, there shouldn't be much activity on this repository.
|
104
62
|
|
105
|
-
|
63
|
+
## Documentation
|
106
64
|
|
107
|
-
The
|
65
|
+
The [documentation is nicely structured](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html)
|
108
66
|
|
109
|
-
|
67
|
+
If you prefer to see it in the code, the main methods are in [this file](https://github.com/MaxLap/activerecord_where_assoc/blob/master/lib/active_record_where_assoc/relation_returning_methods.rb)
|
68
|
+
and the ones that return SQL parts are in [this one](https://github.com/MaxLap/activerecord_where_assoc/blob/master/lib/active_record_where_assoc/sql_returning_methods.rb)
|
110
69
|
|
111
|
-
|
70
|
+
Here are some [usage tips](#usage-tips)
|
112
71
|
|
113
|
-
|
72
|
+
## Usage
|
114
73
|
|
115
|
-
|
74
|
+
You can view [many examples](EXAMPLES.md).
|
116
75
|
|
117
|
-
|
118
|
-
```ruby
|
119
|
-
# Options are passed after the conditions argument
|
120
|
-
Posts.where_assoc_exists(:last_status, nil, ignore_limit: true)
|
121
|
-
Posts.where_assoc_count(1, :<, :last_status, nil, ignore_limit: true)
|
122
|
-
```
|
76
|
+
Otherwise, here is a short explanation of the main methods provided by this gem:
|
123
77
|
|
124
|
-
* As default for everywhere
|
125
78
|
```ruby
|
126
|
-
|
127
|
-
|
79
|
+
where_assoc_exists(association_name, conditions, options, &block)
|
80
|
+
where_assoc_not_exists(association_name, conditions, options, &block)
|
81
|
+
where_assoc_count(left_operand, operator, association_name, conditions, options, &block)
|
128
82
|
```
|
129
83
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
*
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
*
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
*
|
160
|
-
*
|
161
|
-
*
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
*
|
84
|
+
* These methods add a condition (a `#where`) that checks if the association exists (or not)
|
85
|
+
* You can specify condition on the association, so you could check only for comments that are made by an admin.
|
86
|
+
* Each method returns a new relation, meaning you can chain `#where`, `#order`, `limit`, etc.
|
87
|
+
* common arguments:
|
88
|
+
* association_name: the association we are doing the condition on.
|
89
|
+
* conditions: (optional) the condition to apply on the association. It can be anything that `#where` can receive, so: Hash, String and Array (string with binds).
|
90
|
+
* options: [available options](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-Options) to alter some behaviors. (rarely necessary)
|
91
|
+
* block: adds more complex conditions by receiving a relation on the association. Can use `#where`, `#where_assoc_*`, scopes, and other scoping methods.
|
92
|
+
Must return a relation.
|
93
|
+
The block either:
|
94
|
+
* receives no argument, in which case `self` is set to the relation, so you can do `{ where(id: 123) }`
|
95
|
+
* receives arguments, in which case the block is called with the relation as first parameter.
|
96
|
+
|
97
|
+
The block should return the new relation to use or `nil` to do as if there were no blocks.
|
98
|
+
It's common to use `where_assoc_*(..., &:scope_name)` to use a single scope.
|
99
|
+
* `#where_assoc_count` is a generalization of `#where_assoc_exists` and `#where_assoc_not_exists`. It behaves the same way, but is more powerful, as it allows you to specify how many matches there should be.
|
100
|
+
```ruby
|
101
|
+
# These are equivalent:
|
102
|
+
Post.where_assoc_exists(:comments, is_spam: true)
|
103
|
+
Post.where_assoc_count(1, :<=, :comments, is_spam: true)
|
104
|
+
|
105
|
+
Post.where_assoc_not_exists(:comments, is_spam: true)
|
106
|
+
Post.where_assoc_count(0, :==, :comments, is_spam: true)
|
107
|
+
|
108
|
+
# This has no equivalent (Posts with at least 5 spam comments)
|
109
|
+
Post.where_assoc_count(5, :<=, :comments, is_spam: true)
|
110
|
+
```
|
111
|
+
* `where_assoc_count`'s additional arguments
|
112
|
+
The order of the parameters of `#where_assoc_count` may seem confusing, but you will get used to it. It helps to remember: the goal is to do: `5 < (SELECT COUNT(*) FROM ...)`, the number is first, then operator, then the association and its conditions.
|
113
|
+
* left_operand:
|
114
|
+
* a number
|
115
|
+
* a string of SQL to embed in the query
|
116
|
+
* a range (operator must be `:==` or `:!=`)
|
117
|
+
will use SQL's `BETWEEN` or `NOT BETWEEN`
|
118
|
+
supports infinite ranges and exclusive end
|
119
|
+
* operator: one of `:<`, `:<=`, `:==`, `:!=`, `:>=`, `:>`
|
120
|
+
|
121
|
+
## Intuition
|
122
|
+
|
123
|
+
Here is the basic intuition for the methods:
|
124
|
+
|
125
|
+
`#where_assoc_exists` filters the models, returning those *where* a record for the *association* matching a condition (by default any record in the association) *exists*.
|
126
|
+
|
127
|
+
`#where_assoc_not_exists` is the exact opposite of `#where_assoc_exists`. Filters the models, returning those *where* a record for the *association* matching a condition (by default any record in the association) do *not exists*
|
128
|
+
|
129
|
+
`#where_assoc_count` the more specific version of `#where_assoc_exists`. Filters the models, returning those *where* a record for the *association* matching a condition (by default any record in the association) do *not exists*
|
130
|
+
|
131
|
+
The condition that you may need on the record can be quite complicated. For this reason, you can pass a block to these methods.
|
132
|
+
The block will receive a relation on records of the association. Your job is then to call `where` and scopes to specify what you want to exist (or to not exist if using `#where_assoc_not_exists`).
|
133
|
+
|
134
|
+
So if you have `User.where_assoc_exists(:comments) {|rel| rel.where("content ilike '%github.com%'") }`, `rel` is a relation is on `Comment`, and you are specifying what you want to exist. So now we are looking for users that made a comment containing 'github.com'.
|
166
135
|
|
167
136
|
## Usage tips
|
168
137
|
|
@@ -223,15 +192,30 @@ Post.where_assoc_exists([:comments, :author, :address], "addresses.country = pos
|
|
223
192
|
|
224
193
|
Doing the same thing but with less associations between `address` and `posts` would not be an issue.
|
225
194
|
|
195
|
+
### Getting SQL strings
|
196
|
+
|
197
|
+
Sometimes, you may need only the SQL of the condition instead of a whole relation, such as when writing your own complex SQL. There are methods available for this use case: `assoc_exists_sql`, `assoc_not_exists_sql`, `compare_assoc_count_sql`, `only_assoc_count_sql`.
|
198
|
+
|
199
|
+
You can read some more about them in [their documentation](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/SqlReturningMethods.html)
|
200
|
+
|
201
|
+
Here is a simple example of they use. Note that they should always be called on the class.
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
# Users with a post or a comment
|
205
|
+
User.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}")
|
206
|
+
my_users.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}")
|
207
|
+
# Note that this could be achieved in Rails 5 using the #or method and #where_assoc_exists
|
208
|
+
```
|
209
|
+
|
226
210
|
### The opposite of multiple nested EXISTS...
|
227
211
|
|
228
|
-
... is a single `NOT EXISTS` with
|
212
|
+
... is a single `NOT EXISTS` with the nested ones still using `EXISTS`.
|
229
213
|
|
230
214
|
All the methods always chain nested associations using an `EXISTS` when they have to go through multiple hoops. Only the outer-most, or first, association will have a `NOT EXISTS` when using `#where_assoc_not_exists` or a `COUNT` when using `#where_assoc_count`. This is the logical way of doing it.
|
231
215
|
|
232
216
|
### Using `#from` in scope
|
233
217
|
|
234
|
-
If you want to use a scope / condition which uses `#from`, then you need to use the [:never_alias_limit](#
|
218
|
+
If you want to use a scope / condition which uses `#from`, then you need to use the [:never_alias_limit](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-3Anever_alias_limit+option) option to avoid `#where_assoc_*` being overwritten by your scope and getting a weird exception / wrong result.
|
235
219
|
|
236
220
|
## Known issues/limitations
|
237
221
|
|
@@ -240,7 +224,7 @@ On MySQL databases, it is not possible to use `has_one` associations and associa
|
|
240
224
|
|
241
225
|
I do not know of a way to do a SQL query that can deal with all the specifics of `has_one` for MySQL. If you have one, then please suggest it in an issue/pull request.
|
242
226
|
|
243
|
-
In order to work around this, you must use the [ignore_limit](#
|
227
|
+
In order to work around this, you must use the [ignore_limit](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-3Aignore_limit+option) option. The behavior is less correct, but better than being unable to use the gem.
|
244
228
|
|
245
229
|
### has_* :through vs limit/offset
|
246
230
|
For `has_many` and `has_one` with the `:through` option, `#limit` and `#offset` are ignored. Note that `#limit` and `#offset` of the `:source` and of the `:through` side are applied correctly.
|
@@ -255,18 +239,12 @@ Note that the support of `#limit` and `#offset` for the `:source` and `:through`
|
|
255
239
|
|
256
240
|
After checking out the repo, run `bundle install` to install dependencies.
|
257
241
|
|
258
|
-
Run `rake test` to run the tests for the latest version of rails
|
242
|
+
Run `rake test` to run the tests for the latest version of rails. If you want SQL queries printed when you have failures, use `SQL_WITH_FAILURES=1 rake test`.
|
259
243
|
|
260
244
|
Run `bin/console` for an interactive prompt that will allow you to experiment in the same environment as the tests.
|
261
245
|
|
262
246
|
Run `bin/fixcop` to fix a lot of common styling mistake from your changes and then display the remaining rubocop rules you break. Make sure to do this before committing and submitting PRs. Use common sense, sometimes it's okay to break a rule, add a [rubocop:disable comment](http://rubocop.readthedocs.io/en/latest/configuration/#disabling-cops-within-source-code) in that situation.
|
263
247
|
|
264
|
-
Run `bin/testall` to test all supported rails/ruby versions:
|
265
|
-
* It will tell you about missing ruby versions, which you can install if you want to test for them
|
266
|
-
* It will run `rake test` on each supported version or ruby/rails
|
267
|
-
* It automatically installs bundler if a ruby version doesn't have it
|
268
|
-
* It automatically runs `bundle install`
|
269
|
-
|
270
248
|
## Contributing
|
271
249
|
|
272
250
|
Bug reports and pull requests are welcome on GitHub at https://github.com/MaxLap/activerecord_where_assoc.
|