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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordWhereAssoc
4
- VERSION = "0.1.3".freeze
4
+ VERSION = "1.1.2".freeze
5
5
  end
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: 0.1.3
4
+ version: 1.1.2
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: 2018-06-20 00:00:00.000000000 Z
11
+ date: 2020-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -28,14 +28,14 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.15'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.15'
41
41
  - !ruby/object:Gem::Dependency
@@ -68,32 +68,18 @@ dependencies:
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '10.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '10.0'
83
- - !ruby/object:Gem::Dependency
84
- name: coveralls
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - ">="
88
74
  - !ruby/object:Gem::Version
89
- version: '0'
75
+ version: '10.0'
90
76
  type: :development
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
80
  - - ">="
95
81
  - !ruby/object:Gem::Version
96
- version: '0'
82
+ version: '10.0'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: deep-cover
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -172,7 +158,6 @@ executables: []
172
158
  extensions: []
173
159
  extra_rdoc_files: []
174
160
  files:
175
- - ALTERNATIVES_PROBLEMS.md
176
161
  - CHANGELOG.md
177
162
  - EXAMPLES.md
178
163
  - LICENSE.txt
@@ -181,8 +166,9 @@ files:
181
166
  - lib/active_record_where_assoc/active_record_compat.rb
182
167
  - lib/active_record_where_assoc/core_logic.rb
183
168
  - lib/active_record_where_assoc/exceptions.rb
184
- - lib/active_record_where_assoc/query_methods.rb
185
- - lib/active_record_where_assoc/querying.rb
169
+ - lib/active_record_where_assoc/relation_returning_delegates.rb
170
+ - lib/active_record_where_assoc/relation_returning_methods.rb
171
+ - lib/active_record_where_assoc/sql_returning_methods.rb
186
172
  - lib/active_record_where_assoc/version.rb
187
173
  - lib/activerecord_where_assoc.rb
188
174
  homepage: https://github.com/MaxLap/activerecord_where_assoc
@@ -204,8 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
190
  - !ruby/object:Gem::Version
205
191
  version: '0'
206
192
  requirements: []
207
- rubyforge_project:
208
- rubygems_version: 2.7.6
193
+ rubygems_version: 3.0.3
209
194
  signing_key:
210
195
  specification_version: 4
211
196
  summary: Make ActiveRecord do conditions on your associations
@@ -1,221 +0,0 @@
1
- There are multiple ways of achieving results similar to what this gems does using either only built-in ActiveRecord functionalities or other gems.
2
-
3
- This is a list of some of those alternatives, explaining what issues they have or reasons to prefer this gem over them.
4
-
5
-
6
- ## Too long; didn't read
7
-
8
- **Use this gem, you will avoid problems and save time**
9
-
10
- * No more having to choose, case by case, which way has the less problems.
11
- Just use `#where_assoc_*` each time and avoid every problems.
12
- * Need less raw SQL, which means less code, more clarity and less maintenance.
13
- * Allows powerful scopes without traps.
14
- * Handles recursive associations correctly.
15
- * Handles has_one correctly.
16
-
17
- ## Short version
18
-
19
- Summary of the problems of the alternatives that `activerecord_where_assoc` solves. The following sections go in more details.
20
-
21
- * every alternatives (except raw SQL):
22
- * treat `has_one` like a `has_many`.
23
- * can't handle recursive associations. (ex: parent/children)
24
- * no simple way of checking for more complex counts. (such as `less than 5`)
25
- * `joins` / `includes`:
26
- * doing `not exists` with conditions requires a `LEFT JOIN` with the conditions as part of the `ON`, which requires raw SQL.
27
- * checking for 2 sets of conditions on different records of the same association won't work. (so your scopes can be incompatible)
28
- * can't be used with Rails 5's `or` unless both sides do the same `joins` / `includes` / `eager_load`.
29
- * `joins`:
30
- * `has_many` may return duplicate records.
31
- * using `uniq` / `distinct` to solve duplicate rows is an unexpected side-effect when this is in a scope.
32
- * `includes`:
33
- * triggers eagerloading, which makes your `scope` have unexpected bad performances if it's not necessary.
34
- * when using a condition, the eagerloaded records are also filtered, which is very bug-prone when in a scope.
35
- * raw SQL:
36
- * verbose, less clear on the goal of the queries (you don't even name the association the query is about).
37
- * need to repeat conditions from the association / default_scope.
38
- * `where_exists` gem:
39
- * can't use scopes of the association's model.
40
- * can't go deeper than one level of association.
41
-
42
- ## Common problems to most alternatives
43
-
44
- These are problems that affect most alternatives. Details are written in this section and just referred to by a one liner when they apply to an alternative.
45
-
46
- ### Treating has_one like has_many
47
-
48
- Every alternative treats a has_one just like a has_many. So if any of the records (instead of only the first) matches your condition, you will get a match.
49
-
50
- And example to clarify:
51
-
52
- ```ruby
53
- class Person < ActiveRecord::Base
54
- has_many :addresses
55
- has_one :current_address, -> { order("effective_date DESC") }, class_name: 'Address'
56
- end
57
-
58
- # This correctly matches only those whose current_address is in Montreal
59
- Person.where_assoc_exists(:current_address, city: 'Montreal')
60
-
61
- # Every alternatives (except raw SQL):
62
- # Matches those that have had an address in Montreal, no matter when
63
- Person.where_assoc_exists(:addresses, city: 'Montreal')
64
- ```
65
-
66
- The general version of this problem is the handling of `limit` and `offset` on associations and in default_scopes. where_assoc_exists handle those correctly and only checks the records that match the limit and the offset.
67
-
68
- ### Raw SQL joins or sub-selects
69
-
70
- Having to write the joins and conditions in raw SQL is more painful and more error prone than having a method do it for you. It hides the important details of what you are doing in a lot of verbosity.
71
-
72
- If there are conditions set on either the association or a default_scope of the model, then you must rewrite those conditions in your manual joins and your manual sub-selects. Worst, if you add/change those conditions on the association / default_scope, then you must find every raw SQL that apply and do the same operation.
73
-
74
- ```ruby
75
- class Post < ActiveRecord::Base
76
- # Any raw SQL doing a join or sub-select on public_comments, if it want to be representative,
77
- # must repeat "public = true".
78
- has_many :public_comments, -> { where(public: true) }, class_name: 'Comment'
79
- end
80
-
81
- class Comment < ActiveRecord::Base
82
- # Any raw SQL doing a join or sub-select to this model, if it want to be representative,
83
- # must repeat "deleted_at IS NULL".
84
- default_scope -> { where(deleted_at: nil) }
85
- end
86
- ```
87
-
88
- All of this is avoided by where_assoc_* methods.
89
-
90
- ### Unable to handle recursive associations
91
-
92
- When you have recursive associations such as parent/children, you must compare multiple rows of the same table. To do this, you have no choice but to write your own raw SQL to, at the very least, do a SQL join with an alias.
93
-
94
- This brings us back to the [raw SQL joins](#raw-sql-joins-or-sub-selects) problem.
95
-
96
- `where_assoc_*` methods handle this seemlessly.
97
-
98
- ## ActiveRecord only
99
-
100
- Those are the common ways given in stack overflow answers.
101
-
102
- ### Using `joins` and `where`
103
-
104
- ```ruby
105
- Post.where_assoc_exists(:comments, is_spam: true)
106
- Post.joins(:comments).where(comments: {is_spam: true})
107
- ```
108
-
109
- * If the association maps to multiple records (such as with a has_many), then the the relation will return one record for each matching association record. In this example, you would get the same post twice if it has 2 comments that are marked as spam.
110
- Using `uniq` can solve this issue, but if you do that in a scope, then that scope unexpectedly adds a DISTINCT to your query, which can lead to unexpected results if you actually wanted duplicates for a different reason.
111
-
112
- * Doing the opposite is a lot more complicated, as seen below. You have to include your conditions directly in the join and use a LEFT JOIN, this means writing the whole thing in raw SQL, and then you must check for the id of the association to be empty.
113
-
114
- ```ruby
115
- Post.where_assoc_not_exists(:comments, is_spam: true)
116
- Post.joins("LEFT JOIN comments ON posts.id = comments.post_id AND comments.is_spam = true").where(comments: {id: nil})
117
- ```
118
-
119
- Writing a raw join like that has yet more problems: [raw SQL joins](#raw-sql-joins-or-sub-selects)
120
-
121
- * If you want to have another condition referring to the same association (or just the same table), then you need to write out the SQL for the second join using an alias. Therefore, your scopes are not even compatible unless each of them has a join with a unique alias.
122
-
123
- ```ruby
124
- # We want to be able to match either different or the same records
125
- Post.where_assoc_exists(:comments, is_spam: true)
126
- .where_assoc_exists(:comments, is_reported: true)
127
-
128
- # Please don't ever do this, this just shows how painful it would be
129
- # If you reach the need to do this but won't use where_assoc_exists,
130
- # go for a regular #where("EXISTS( SELECT ...)")
131
- Post.joins(:comments).where(comments: {is_spam: true})
132
- .joins("JOIN comments comments_for_reported ON posts.id = comments_for_reported.post_id")
133
- .where(comments_for_reported: {is_reported: true})
134
- ```
135
-
136
- * Cannot be used with Rails 5's `or` unless both side do the same `joins`.
137
- * [Treats has_one like a has_many](#treating-has_one-like-has_many)
138
- * [Can't handle recursive associations](#unable-to-handle-recursive-associations)
139
-
140
- ### Using `includes` (or `eager_load`) and `where`
141
-
142
- This solution is similar to the `joins` one above, but avoids the need for `uniq`. Every other problems of the `joins` remain. You also add other potential issues.
143
-
144
- ```ruby
145
- Post.where_assoc_exists(:comments, is_spam: true)
146
- Post.eager_load(:comments).where(comments: {is_spam: true})
147
- ```
148
-
149
- * You are triggering the loading of potentially lots of records that you might not need. You don't expect a scope like `have_reported_comments` to trigger eager loading. This is a performance degradation.
150
-
151
- * The eager loaded records of the association are actually also filtered by the conditions. All of the posts returned will only have the comments that are spam.
152
- This means if you iterate on `Post.have_reported_comments` to display each of the comments of the posts that have at least one reported comment, you are actually only going to display the reported comments. This may be what you wanted to do, but it clearly isn't intuitive.
153
-
154
- * Cannot be used with Rails 5's `or` unless both side do the same `includes` or `eager_load`.
155
-
156
- * [Treats has_one like a has_many](#treating-has_one-like-has_many)
157
- * [Can't handle recursive associations](#unable-to-handle-recursive-associations)
158
-
159
- * Simply cannot be used for complex cases.
160
-
161
- Note: using `includes` (or `eager_load`) already does a LEFT JOIN, so it is pretty easy to do a "not exists", but only if you don't need any condition on the association (which would normally need to be in the JOIN clause):
162
-
163
- ```ruby
164
- Post.where_assoc_exists(:comments)
165
- Post.eager_load(:comments).where(comments: {id: nil})
166
- ```
167
-
168
- ### Using `where("EXISTS( SELECT... )")`
169
-
170
- This is what is gem does behind the scene, but doing it manually can lead to troubles:
171
-
172
- * Problems with writing [raw SQL sub-selects](#raw-sql-joins-or-sub-selects)
173
-
174
- * Unless you do a quite complex nested sub-selects, you will [treat has_one like a has_many](#treating-has_one-like-has_many)
175
-
176
-
177
- ## Gems
178
-
179
- ### where_exists
180
-
181
- https://github.com/EugZol/where_exists
182
-
183
- An interesting gem that also does `EXISTS (SELECT ... )`behind the scene. Solves most issues from ActiveRecord only alternatives, but appears less powerful than where_assoc_exists.
184
-
185
- * where_exists supports polymorphic belongs_to. This is something that where_assoc doesn't do at the moment.
186
- However, the way it does this is by doing a pluck on the type column, which in some situation could be a slow query if there is a lots of rows to scan.
187
-
188
- * Unable to use scopes of the association's model.
189
- ```ruby
190
- # There is no equivalent for this (admins is a scope on User)
191
- Comment.where_assoc_exists(:author, &:admins)
192
- ```
193
-
194
- * Cannot use a block for more complex conditions
195
- ```ruby
196
- # There is no equivalent for this
197
- Comment.where_assoc_exists(:author) { admins.where("created_at <= ?", 1.month.ago) }
198
- ```
199
-
200
- * Unable to dig deeper in the associations
201
- Note: it does follow :through associations so doing a custom associations for your need can be a workaround.
202
-
203
- ```ruby
204
- # There is no equivalent for this (Users that have posts with at least a comments)
205
- User.where_assoc_exists([:posts, :comments])
206
- ```
207
-
208
- * Has no equivalent to `where_assoc_count`
209
- ```ruby
210
- # There is no equivalent for this (posts with more than 5 comments)
211
- Post.where_assoc_count(:comments, :>, 5)
212
- ```
213
-
214
- * [Treats has_one like a has_many](#treating-has_one-like-has_many)
215
-
216
- * [Can't handle recursive associations](#unable-to-handle-recursive-associations)
217
-
218
- * `where_exists` is shorter than `where_assoc_exists`, but it is also less obvious about what it does.
219
- In any case, it is trivial to alias one name to the other one.
220
-
221
- * where_exists supports Rails 4.2 and up, while where_assoc supports Rails 4.1 and up.
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "active_record_compat"
4
- require_relative "exceptions"
5
-
6
- module ActiveRecordWhereAssoc
7
- module QueryMethods
8
- # Returns a new relation, which is the result of filtering the current relation
9
- # based on if a record for the specified association of the model exists. Conditions
10
- # the associated model must match to count as existing can also be specified.
11
- #
12
- # Here is a quick overview of the arguments received followed by a detailed explanation
13
- # along with more examples. You may also consider viewing the gem's README. It contains
14
- # known issues and some tips. The readme is packaged with the gem and viewable on github:
15
- # https://github.com/MaxLap/activerecord_where_assoc
16
- #
17
- #
18
- # As 1st argument, you must specify the association to check against. This can be
19
- # any of the associations on the current relation's model.
20
- #
21
- # # Posts that have at least one comment
22
- # Post.where_assoc_exists(:comments)
23
- #
24
- # As 2nd argument, you can add conditions that the records in the association must match
25
- # to be considered as existing.
26
- #
27
- # The 3rd argument is for options that alter how the query is generated.
28
- #
29
- # If your conditions are too complex or too long to be placed in the 2nd argument,
30
- # #where_assoc_* accepts a block in which you can do anything you want on the relation
31
- # (any scoping method such as #where, #joins, nested #where_assoc_*, scopes of the model).
32
- #
33
- # === the association argument (1st argument)
34
- #
35
- # This is the association you want to check if records exists. If you want, you can pass
36
- # an array of associations. They will be followed in order, just like a has_many :through
37
- # would.
38
- #
39
- # # Posts with at least one comment
40
- # Post.where_assoc_exists(:comments)
41
- #
42
- # # Posts for which there is at least one reply to a comment.
43
- # Post.where_assoc_exists([:comments, :replies])
44
- #
45
- # Note that if you use conditions / blocks, they will only be applied to the last
46
- # association of the array. If you want something else, you will need to use
47
- # the block argument to nest multiple calls to #where_assoc_exists
48
- #
49
- # # Post.where_assoc_exists(:comments) { where_assoc_exists(:replies) }
50
- #
51
- # === the condition argument (2nd argument)
52
- #
53
- # This argument is additional conditions the association's records must fulfill to be
54
- # considered as "existing". The argument is passed directly to #where.
55
- #
56
- # # Posts that have at least one comment considered as spam
57
- # # Using a Hash
58
- # Post.where_assoc_exists(:comments, is_spam: true)
59
- #
60
- # # Using a String
61
- # Post.where_assoc_exists(:comments, "is_spam = true")
62
- #
63
- # # Using an Array (a string and its binds)
64
- # Post.where_assoc_exists(:comments, ["is_spam = ?", true])
65
- #
66
- # If the condition argument is blank, it is ignored (just like #where does).
67
- #
68
- # === the options argument (3rd argument)
69
- #
70
- # Some options are available to tweak how things queries are generated. In some case, this
71
- # also changes the results of the query.
72
- #
73
- # ignore_limit: when true, #limit and #offset that are set either from default_scope or
74
- # on associations are ignored. #has_one means #limit(1), so this makes
75
- # #has_one be treated like #has_many.
76
- #
77
- # never_alias_limit: when true, #where_assoc_* will not use #from to build relations that
78
- # have #limit or #offset set on default_scope or on associations.
79
- # Note, #has_one means #limit(1), so it will also use #from unless this
80
- # option is activated.
81
- #
82
- # === the block
83
- #
84
- # The block is used to add more complex conditions. The result behaves the same way
85
- # as the 2nd argument's conditions, but lets you use any scoping methods, such as
86
- # #where, #joins, # nested #where_assoc_* and scopes of the model. Note that using
87
- # #joins might lead to unexpected results when using #where_assoc_count, since if
88
- # the joins adds rows, it will change the resulting count.
89
- #
90
- # There are 2 ways of using the block for adding conditions to the association.
91
- #
92
- # * A block that receives one argument
93
- # The block receives a relation on the target association and return a relation with added
94
- # filters or may return nil to do nothing.
95
- #
96
- # # Using a where for the added condition
97
- # Post.where_assoc_exists(:comments) { |comments| comments.where(is_spam: true) }
98
- #
99
- # # Applying a scope of the relation
100
- # Post.where_assoc_exists(:comments) { |comments| comments.spam_flagged }
101
- #
102
- # # Applying a scope of the relation, using the &:shortcut for procs
103
- # Post.where_assoc_exists(:comments, &:spam_flagged)
104
- #
105
- #
106
- # * A block that receives no argument
107
- # Instead of receiving the relation as argument, the relation is used as the "self" of
108
- # the block. Everything else is identical to the block with one argument.
109
- #
110
- # # Using a where for the added condition
111
- # Post.where_assoc_exists(:comments) { where(is_spam: true) }
112
- #
113
- # # Applying a scope of the relation
114
- # Post.where_assoc_exists(:comments) { spam_flagged }
115
- #
116
- # The main reason to use a block with an argument instead of without is when you need
117
- # to call methods on the self outside of the block, such as:
118
- #
119
- # Post.where_assoc_exists(:comments) { |comments| comments.where(id: self.something) }
120
- #
121
- def where_assoc_exists(association_name, given_scope = nil, options = {}, &block)
122
- ActiveRecordWhereAssoc::CoreLogic.do_where_assoc_exists(self, association_name, given_scope, options, &block)
123
- end
124
-
125
- # Returns a new relation, which is the result of filtering the current relation
126
- # based on if a record for the specified association of the model doesn't exist.
127
- # Conditions the associated model must match to count as existing can also be specified.
128
- #
129
- # The parameters and everything is identical to #where_assoc_exists. The only
130
- # difference is that a record is matched if no matching association record that
131
- # fulfill the conditions are found.
132
- def where_assoc_not_exists(association_name, given_scope = nil, options = {}, &block)
133
- ActiveRecordWhereAssoc::CoreLogic.do_where_assoc_not_exists(self, association_name, given_scope, options, &block)
134
- end
135
-
136
- # Returns a new relation, which is the result of filtering the current relation
137
- # based on how many records for the specified association of the model exists. Conditions
138
- # the associated model must match can also be specified.
139
- #
140
- # #where_assoc_count is a generalization of #where_assoc_exists and #where_assoc_not_exists.
141
- # It behave behaves the same way as them, but is more flexible as it allows you to be
142
- # specific about how many matches there should be. To clarify, here are equivalent examples:
143
- #
144
- # Post.where_assoc_exists(:comments)
145
- # Post.where_assoc_count(1, :<=, :comments)
146
- #
147
- # Post.where_assoc_not_exists(:comments)
148
- # Post.where_assoc_count(0, :==, :comments)
149
- #
150
- # The usage is the same as with #where_assoc_exists, however, 2 arguments are inserted
151
- # at the beginning.
152
- #
153
- # 1st argument: the left side of the comparison. One of:
154
- # a number
155
- # a string of SQL to embed in the query
156
- # a range (operator must be :== or :!=), will use BETWEEN or NOT BETWEEN
157
- # supports infinite ranges and exclusive end
158
- # 2nd argument: the operator to use: :<, :<=, :==, :!=, :>=, :>
159
- # 3rd, 4th and 5th arguments: same as #where_assoc_exists' 1st, 2nd and 3rd arguments
160
- # block: same as #where_assoc_exists' block
161
- #
162
- # The order of the parameters may seem confusing. But you will get used to it. To help
163
- # remember the order of the parameters, remember that the goal is to do:
164
- # 5 < (SELECT COUNT(*) FROM ...)
165
- # So the parameters are in the same order as in that query: number, operator, association.
166
- #
167
- # To be clear, when you use multiple associations in an array, the count you will be
168
- # comparing against is the total number of records of that last association.
169
- #
170
- # # The users that have received at least 5 comments total on all of their posts
171
- # # So this can be one post that has 5 comments of 5 posts with 1 comments
172
- # User.where_assoc_count(5, :<=, [:posts, :comments])
173
- #
174
- # # The users that have at least 5 posts with at least one comments
175
- # User.where_assoc_count(5, :<=, :posts) { where_assoc_exists(:comments) }
176
- def where_assoc_count(left_operand, operator, association_name, given_scope = nil, options = {}, &block)
177
- ActiveRecordWhereAssoc::CoreLogic.do_where_assoc_count(self, left_operand, operator, association_name, given_scope, options, &block)
178
- end
179
- end
180
- end