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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0805c22b1a2c499a1d642f4bf4c624e31318192e2243a38ef3dca92cadce2a58'
4
- data.tar.gz: bbebf2d6e4402c07a54af54fa87c25c5f94c7813e4b0cc6cc63a761bd291cb7c
3
+ metadata.gz: c33267f5201fd0465cceec484dd0a2c91b3511b47289f500311e0d3a3946b28f
4
+ data.tar.gz: 0b1ef81c3b3106a0226ed72494523ce3a2ebb005f4b1401d6cca3917a78bfa26
5
5
  SHA512:
6
- metadata.gz: b17588d07e22a79132e62f6341fad4b4e39106ec2b957bed866d85ae3e20c59de95efae7ea1a91cdd812111c40afda385277800a90fc33b95ec14ad5af9f9bdb
7
- data.tar.gz: b34651a306ac5503fa96bf30058c00e1b071f9bcb5bc429b249c7455a2d8ec6d6fe6518c95c6060aa507f3fb59bfe44434aba4c5fc0e1ffec6b25c673209e13f
6
+ metadata.gz: 156df4226e93a9d5a8e900a71b518d42ff984a47a8917d7d807429d3b473a5f8db7d392496c8f4dfda7f0746689b9127547d238b2f875846235c9075e4789f7b
7
+ data.tar.gz: fbea2c6d875f87c5b04d740fb6878d1f9ece9e11ba0053d38bbe6c8cb6311b194be770ebd402bc59a2baad10c3ecac8ee9f5e2bad8f6147ea152d8e8c2a96518
@@ -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.
@@ -1,8 +1,10 @@
1
- Here are some example usages of the gem, along with the generated SQL. Each of those can be chained with scoping methods.
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 from running `ruby examples/examples.rb`
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
- WHERE (EXISTS (
18
- SELECT 1 FROM "comments"
19
- WHERE "comments"."post_id" = "posts"."id"
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
- WHERE (NOT EXISTS (
32
- SELECT 1 FROM "comments"
33
- WHERE "comments"."post_id" = "posts"."id"
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
- WHERE ((50) <= COALESCE((
46
- SELECT COUNT(*) FROM "comments"
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
- ), 0))
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
- WHERE "comments"."post_id" = 1 AND (EXISTS (
62
- SELECT 1 FROM "users"
63
- WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
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
- WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
76
- SELECT 1 FROM "users"
77
- WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
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
- WHERE ((5) <= COALESCE((
90
- SELECT COUNT(*) FROM "comments"
91
- WHERE "comments"."post_id" = "posts"."id" AND (is_reported = 't')
92
- ), 0))
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
- WHERE (EXISTS (
104
- SELECT 1 FROM "users"
105
- WHERE "users"."id" = "posts"."author_id" AND (is_admin = 't')
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
- WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
118
- SELECT 1 FROM "users"
119
- WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
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
- WHERE (COALESCE((
132
- SELECT COUNT(*) FROM "comments"
133
- WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't'
134
- ), 0) BETWEEN 5 AND 10)
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
- WHERE (EXISTS (
146
- SELECT 1 FROM "posts"
147
- WHERE "posts"."id" = "comments"."post_id" AND "posts"."author_id" = 1
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
- WHERE (EXISTS (
162
- SELECT 1 FROM "comments"
163
- WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
164
- SELECT 1 FROM "users"
165
- WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
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 (use conditions between posts)
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
- WHERE (EXISTS (
179
- SELECT 1 FROM "comments"
180
- WHERE "comments"."post_id" = "posts"."id" AND (posts.author_id = comments.author_id)
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
- WHERE (EXISTS (
195
- SELECT 1 FROM "comments"
196
- WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't' AND (EXISTS (
197
- SELECT 1 FROM "users"
198
- WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
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
- WHERE "posts"."author_id" = 1 AND (EXISTS (
213
- SELECT 1 FROM "comments"
214
- WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't'
215
- )) AND (EXISTS (
216
- SELECT 1 FROM "comments"
217
- WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
218
- SELECT 1 FROM "users"
219
- WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
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
- [![Build Status](https://travis-ci.org/MaxLap/activerecord_where_assoc.svg?branch=master)](https://travis-ci.org/MaxLap/activerecord_where_assoc)
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 provides powerful methods to add conditions based on the associations of your records. (Using SQL's `EXISTS` operator)
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.where(is_spam: false) }.where(...)
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
- You also avoid many [problems with the alternative options](ALTERNATIVES_PROBLEMS.md).
21
+ Here is an [introduction to this gem](INTRODUCTION.md).
24
22
 
25
- Works with SQLite3, PostgreSQL and MySQL. [MySQL has one limitation](#mysql-doesnt-support-sub-limit). Untested with other RDBMS.
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
- ## Feedback
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
- Since the gem is brand new, I'm releasing as 0.1 before bumping to 1.0 once I have some feedback. There is an extensive test suite testing for lots of cases, making me quite confident that it can handle most of the cases you can throw at it.
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
- ## Usage
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
- The order of the parameters may seem confusing, but you will get used to it. To help remember the order of the parameters, remember that the goal is to do:
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
- 5 < (SELECT COUNT(*) FROM ...)
63
+ ## Documentation
106
64
 
107
- The parameters are in the same order as in that query: number, operator, association.
65
+ The [documentation is nicely structured](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html)
108
66
 
109
- ### More examples
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
- You can view [more usage examples](EXAMPLES.md).
70
+ Here are some [usage tips](#usage-tips)
112
71
 
113
- ### Options
72
+ ## Usage
114
73
 
115
- Each of the methods above can take an options argument. It is also possible to change the default value for the options.
74
+ You can view [many examples](EXAMPLES.md).
116
75
 
117
- * On a per-call basis:
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
- # Somewhere in your setup code, such as an initializer in Rails
127
- ActiveRecordWhereAssoc.default_options[:ignore_limit] = true
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
- Here is a list of the available options:
131
-
132
- #### :ignore_limit
133
-
134
- When this option is true, then `#limit` and `#offset` that are set either from default_scope or on associations are ignored. `#has_one` means `limit(1)`, so `#has_one` will behave like `#has_many` with this option.
135
-
136
- Main reasons to use this:
137
- * This is needed for MySQL to be able to do anything with `#has_one` associations because [MySQL doesn't support sub-limit](#mysql-doesnt-support-sub-limit).
138
- * You have a `#has_one` association which you know can never have more than one record. Using `:ignore_limit`, you will use the simpler query of `#has_many`, which can be more efficient.
139
-
140
- Why this isn't the default:
141
- * From very few tests, the aliasing way seems to produce better plans.
142
- * Using aliasing produces a shorter query.
143
-
144
- #### :never_alias_limit
145
-
146
- When this option is true, `#where_assoc_*` will not use `#from` to build relations that have `#limit` or `#offset` set on default_scope or on associations. Note, `#has_one` means `limit(1)`, so it will also use `#from` unless this option is activated.
147
-
148
- Main reasons to use this:
149
- * You have to use `#from` as condition for `#where_assoc_*` method (possibly because a scope needs it).
150
- * This might result in a difference execution plan for the query since the query ends up being quite different.
151
-
152
- ## Supported Rails versions
153
-
154
- Rails 4.1 to 5.2 are supported with Ruby 2.1 to 2.5.
155
-
156
- ## Advantages
157
-
158
- These methods have many advantages over the alternative ways of achieving the similar results:
159
- * Avoids the [problems with the alternative ways](ALTERNATIVES_PROBLEMS.md)
160
- * Can be chained and nested with regular ActiveRecord methods (`where`, `merge`, `scope`, etc).
161
- * Adds a single condition in the `WHERE` of the query instead of complex things like joins.
162
- * So it's easy to have multiple conditions on the same association
163
- * Handles `has_one` correctly: only testing the "first" record of the association that matches the default_scope and the scope on the association itself.
164
- * Handles recursive associations (such as parent/children) seemlessly.
165
- * Can be used to quickly generate a SQL query that you can edit/use manually.
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 then nested ones still using `EXISTS`.
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](#never_alias_limit) option to avoid `#where_assoc_*` being overwritten by your scope and getting a weird exception / wrong result.
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](#ignore_limit) option. The behavior is less correct, but better than being unable to use the gem.
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.