activerecord_where_assoc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 95ce5c0d5802b14a7e1427d949bd45fec4156841
4
+ data.tar.gz: 928080180b366f561b01cb26f7cf044187ac1c2b
5
+ SHA512:
6
+ metadata.gz: d7e8dccc5e31da6f30a2355994b8ec611de26b015c3786524408f2fc6afb688e31381e71a8908f5df8682637c473a0a55a1d7f6769772389592cb706c316c3e1
7
+ data.tar.gz: a31a92215befe043a240074e0b358502e943cfb097b1409155193497b6f69ec02251dd9d53c7af3ecf723d7d398c543bff598ba15bd4216ae79ed89838192e30
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Maxime Handfield Lapointe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,301 @@
1
+ # ActiveRecord Where Assoc
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)
5
+ [![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
+
8
+ This gem provides powerful methods to add conditions based on the associations of your records. (Using SQL's `EXISTS` operator)
9
+
10
+ ```ruby
11
+ # Find my_post's comments that were not made by an admin
12
+ my_post.comments.where_assoc_not_exists(:author, is_admin: true).where(...)
13
+
14
+ # Find posts that have comments by an admin
15
+ 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(spam: false) }.where(...)
19
+ ```
20
+
21
+ These allow for powerful, chainable, clear and easy to reuse queries. (Great for scopes)
22
+
23
+ You also avoid many [problems with the alternative options](ALTERNATIVES_PROBLEMS.md).
24
+
25
+ Works with SQLite3, PostgreSQL and MySQL. [MySQL has one limitation](#mysql-doesnt-support-sub-limit). Untested with other DBMS.
26
+
27
+ ## Feedback
28
+
29
+ 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.
30
+
31
+ * Failure stories, if you had difficulties that kept you from using the gem.
32
+ * Success stories, if you are using it and things are going great, I wanna hear this too.
33
+ * Suggestions to make the documentation easier to follow / more complete.
34
+
35
+
36
+ ## 0.1.0
37
+
38
+ Since the gem is brand new, I'm releasing 0.1.0 as a public beta before bumping to 1.0.0 once I have some feedback.
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'activerecord_where_assoc'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ $ bundle install
51
+
52
+ Or install it yourself as:
53
+
54
+ $ gem install activerecord_where_assoc
55
+
56
+ ## Usage
57
+
58
+ ### `#where_assoc_exists` & `#where_assoc_not_exists`
59
+
60
+ 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.
61
+
62
+ ```ruby
63
+ Post.where_assoc_exists(:comments, spam: true)
64
+ Post.where_assoc_not_exists(:comments, spam: true)
65
+ ```
66
+
67
+ * 1st parameter: the association we are doing the condition on.
68
+ * 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).
69
+ * 3rd parameter: [options (listed below)](#options) to alter some behaviors.
70
+ * block: adds more complex conditions by receiving a relation on the association. Can apply `#where`, `#where_assoc_*`, scopes, and other scoping methods.
71
+ The block either:
72
+
73
+ * receives no argument, in which case `self` is set to the relation, so you can do `{ where(id: 123) }`
74
+ * receives arguments, in which case the block is called with the relation as first parameter
75
+
76
+ The block should return the new relation to use or `nil` to do as if there were no blocks
77
+ It's common to use `where_assoc_*(..., &:scope_name)` to apply a single scope quickly
78
+
79
+ ### `#where_assoc_count`
80
+
81
+ This is a generalization of `#where_assoc_exists` and `#where_assoc_not_exists`. It behave 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:
82
+
83
+ ```ruby
84
+ Post.where_assoc_exists(:comments, spam: true)
85
+ Post.where_assoc_count(1, :<=, :comments, spam: true)
86
+
87
+ Post.where_assoc_not_exists(:comments, spam: true)
88
+ Post.where_assoc_count(0, :==, :comments, spam: true)
89
+ ```
90
+
91
+ * 1st parameter: a number or any string of SQL to embed in the query used for the leftoperand of the comparison.
92
+ * 2nd parameter: the operator to use: `:<`, `:<=`, `:==`, `:!=`, `:>=`, `:>`
93
+ * 3rd, 4th, 5th parameters are the same as the 1st, 2nd and 3rd parameters of `#where_assoc_exists`.
94
+ * block: same as `#where_assoc_exists`' block
95
+
96
+ 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:
97
+
98
+ 5 < (SELECT COUNT(*) FROM ...)
99
+
100
+ The parameters are in the same order as in that query: number, operator, association.
101
+
102
+ ### Options
103
+
104
+ Each of the methods above can take an options argument. It is also possible to change the default value for the options.
105
+
106
+ * On a per-call basis:
107
+ ```ruby
108
+ # Options are passed after the conditions argument
109
+ Posts.where_assoc_exists(:last_status, nil, ignore_limit: true)
110
+ Posts.where_assoc_count(1, :<, :last_status, nil, ignore_limit: true)
111
+ ```
112
+
113
+ * As default for everywhere
114
+ ```ruby
115
+ # Somewhere in your setup code, such as an initializer in Rails
116
+ ActiveRecordWhereAssoc.default_options[:ignore_limit] = true
117
+ ```
118
+
119
+ Here is a list of the available options:
120
+
121
+ #### :ignore_limit
122
+
123
+ 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.
124
+
125
+ Main reasons to use this:
126
+ * 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).
127
+ * 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.
128
+
129
+ Why this isn't the default:
130
+ * From very few tests, the aliasing way seems to produce better plans.
131
+ * Using aliasing produces a shorter query.
132
+
133
+ #### :never_alias_limit
134
+
135
+ 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.
136
+
137
+ Main reasons to use this:
138
+ * You have to use `#from` as condition for `#where_assoc_*` method (possibly because a scope needs it).
139
+ * This might result in a difference execution plan for the query since the query ends up being quite different.
140
+
141
+ ## Supported Rails versions
142
+
143
+ Rails 4.1 to 5.2 are supported with Ruby 2.1 to 2.5.
144
+
145
+ ## Advantages
146
+
147
+ These methods have many advantages over the alternative ways of achieving the similar results:
148
+ * Avoids the [problems with the alternative ways](ALTERNATIVES_PROBLEMS.md)
149
+ * Can be chained and nested with regular ActiveRecord methods (`where`, `merge`, `scope`, etc).
150
+ * Adds a single condition in the `WHERE` of the query instead of complex things like joins.
151
+ * So it's easy to have multiple conditions on the same association
152
+ * Handles `has_one` correctly: only testing the "first" record of the association that matches the default_scope and the scope on the association itself.
153
+ * Handles recursive associations (such as parent/children) seemlessly.
154
+ * Can be used to quickly generate a SQL query that you can edit/use manually.
155
+
156
+ ## More examples
157
+
158
+ High level explanation of various ways of using the methods. Also take a look at [usage tips](#usage-tips)
159
+
160
+ ```ruby
161
+ # Find my_post's comments that were not made by an admin
162
+ # Uses a Hash for the condition
163
+ my_post.comments.where_assoc_not_exists(:author, is_admin: true)
164
+
165
+ # Find my_user's posts that have comments by an admin
166
+ # Uses an array as shortcut to go to a nested related
167
+ # Uses the block shortcut to use a scope that exists on Author
168
+ my_user.posts.where_assoc_exists([:comments, :author], &:admins).where(...)
169
+
170
+ # Find my_user's posts that have at least 5 non-spam comments
171
+ # Uses a block with a parameter to do a condition
172
+ my_user.posts.where_assoc_count(5, :>=, :comments) { |s| s.where(spam: false) }
173
+
174
+ # Find my_user's posts that have at least 5 non-spam comments
175
+ # Uses a block without parameters to do a condition
176
+ my_user.posts.where_assoc_count(5, :>=, :comments) { where(spam: false) }
177
+
178
+ # Find my_user's posts that have comments by an honest admin
179
+ # Uses multiple associations.
180
+ # Uses a hash as 2nd parameter to do the conditions
181
+ my_user.posts.where_assoc_exists([:comments, :author], honest: true, is_admin: true)
182
+
183
+ # Find any post that has reached its maximum number of allowed comments
184
+ # Uses a string on the left side (first parameter) to refer to a column in the previous table.
185
+ Post.where_assoc_count("posts.max_comments_allowed", :==, :comments)
186
+ ```
187
+
188
+ ## Usage tips
189
+
190
+ ### Nested associations
191
+
192
+ Sometimes, there isn't a single association that goes deep enough. In that situation, you can simply nest the scopes:
193
+
194
+ ```ruby
195
+ # Find users that have a post that has a comment that was made by an admin.
196
+ # Using &:admins to use the admins scope (or any other class method of comments)
197
+ User.where_assoc_exists(:posts) { |posts|
198
+ posts.where_assoc_exists(:comments) { |comments|
199
+ comments.where_assoc_exists(:author, &:admins)
200
+ }
201
+ }
202
+ ```
203
+
204
+ If you don't need special conditions on any of the intermediary associations, then you can an array as shortcut for multiple steps:
205
+
206
+ ```ruby
207
+ # Same as above
208
+ User.where_assoc_exists([:posts, :comments, :author], &:admins)
209
+ ```
210
+
211
+ This shortcut can be used for every `where_assoc_*` methods. The conditions and the block will only be applied to the last association of the chain.
212
+
213
+
214
+ ### Beware of spreading conditions on multiple calls
215
+
216
+ The following have different meanings:
217
+
218
+ ```ruby
219
+ my_user.posts.where_assoc_exists(:comments_authors, is_admin: true, honest: true)
220
+
221
+ my_user.posts.where_assoc_exists(:comments_authors, is_admin: true)
222
+ .where_assoc_exists(:comments_authors, honest: true)
223
+ ```
224
+
225
+ The first is the posts of `my_user` that have a comment made by an honest admin. It requires a single comment to match every conditions.
226
+
227
+ The second is the posts of `my_user` that have a comment made by an admin and a comment made by someone honest. It could be the same comment (like the first query) but it could also be 2 different comments.
228
+
229
+ ### Inter-table conditions
230
+
231
+ It's possible, with string conditions, to refer to all the tables that are used before the association, including the source model.
232
+
233
+ ```ruby
234
+ # Find posts where the author also commented on the post.
235
+ Post.where_assoc_exists(:comments, "posts.author_id = comments.author_id")
236
+ ```
237
+
238
+ Note that some database systems limit how far up you can refer to tables in nested queries. Meaning it's possible that the following query may get refused because of those limits:
239
+
240
+ ```ruby
241
+ # it's hard to come up with a good example...
242
+ Post.where_assoc_exists([:comments, :author, :address], "addresses.country = posts.database_country")
243
+ ```
244
+
245
+ Doing the same thing but with less associations between `address` and `posts` would not be an issue.
246
+
247
+ ### The opposite of multiple nested EXISTS...
248
+
249
+ ... is a single `NOT EXISTS` with then nested ones still using `EXISTS`.
250
+
251
+ 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.
252
+
253
+ ### Using `#from` in scope
254
+
255
+ 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.
256
+
257
+ ## Known issues/limitations
258
+
259
+ ### MySQL doesn't support sub-limit
260
+ On MySQL databases, it is not possible to use `has_one` associations and associations with a scope that apply either a limit or an offset.
261
+
262
+ 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.
263
+
264
+ 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.
265
+
266
+ ### has_* :through vs limit/offset
267
+ 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.
268
+
269
+ This is the opposite of what `ActiveRecord` does when you fetch the result of such an association. `ActiveRecord` will ignore the limits of the part `:source` and of the `:through` and only use the one of the `has_* :through`.
270
+
271
+ It is pretty complicated to support `#limit` and `#offset` of the `has_* :through` and would require quite a bit of refactoring. PR welcome
272
+
273
+ 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.
274
+
275
+ ## Development
276
+
277
+ After checking out the repo, run `bundle install` to install dependencies.
278
+
279
+ Run `rake test` to run the tests for the latest version of rails
280
+
281
+ Run `bin/console` for an interactive prompt that will allow you to experiment in the same environment as the tests.
282
+
283
+ 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.
284
+
285
+ Run `bin/testall` to test all supported rails/ruby versions:
286
+ * It will tell you about missing ruby versions, which you can install if you want to test for them
287
+ * It will run `rake test` on each supported version or ruby/rails
288
+ * It automatically installs bundler if a ruby version doesn't have it
289
+ * It automatically runs `bundle install`
290
+
291
+ ## Contributing
292
+
293
+ Bug reports and pull requests are welcome on GitHub at https://github.com/MaxLap/activerecord_where_assoc.
294
+
295
+ ## License
296
+
297
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
298
+
299
+ ## Acknowledgements
300
+
301
+ * [René van den Berg](https://github.com/ReneB) for some of the code of [activerecord-like](https://github.com/ReneB/activerecord-like) used for help with setting up the tests
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_where_assoc/version"
4
+ require "active_record"
5
+
6
+ module ActiveRecordWhereAssoc
7
+ # Default options for the gem. Meant to be modified in place by external code
8
+ def self.default_options
9
+ @default_options ||= {
10
+ ignore_limit: false,
11
+ never_alias_limit: false,
12
+ }
13
+ end
14
+ end
15
+
16
+ require "active_record_where_assoc/core_logic"
17
+ require "active_record_where_assoc/query_methods"
18
+ require "active_record_where_assoc/querying"
19
+
20
+ ActiveSupport.on_load(:active_record) do
21
+ ActiveRecord.eager_load!
22
+
23
+ # Need to use #send for the include to support Ruby 2.0
24
+ ActiveRecord::Relation.send(:include, ActiveRecordWhereAssoc::QueryMethods)
25
+ ActiveRecord::Base.extend(ActiveRecordWhereAssoc::Querying)
26
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordWhereAssoc
4
+ module ActiveRecordCompat
5
+ if ActiveRecord.gem_version >= Gem::Version.new("5.1")
6
+ def self.join_keys(reflection)
7
+ reflection.join_keys
8
+ end
9
+ elsif ActiveRecord.gem_version >= Gem::Version.new("4.2")
10
+ def self.join_keys(reflection)
11
+ reflection.join_keys(reflection.klass)
12
+ end
13
+ else
14
+ # 4.1 change that introduced JoinKeys:
15
+ # https://github.com/rails/rails/commit/5823e429981dc74f8f53187d2ab573823381bf28#diff-523caff658498027f61cae9d91c8503dL108
16
+ JoinKeys = Struct.new(:key, :foreign_key)
17
+ def self.join_keys(reflection)
18
+ if reflection.source_macro == :belongs_to
19
+ # The original code had to handle polymorphic here. But we don't support polymorphic belongs_to
20
+ # So the code would never reach here in the polymorphic case.
21
+ key = reflection.association_primary_key
22
+ foreign_key = reflection.foreign_key
23
+ else
24
+ key = reflection.foreign_key
25
+ foreign_key = reflection.active_record_primary_key
26
+ end
27
+
28
+ JoinKeys.new(key, foreign_key)
29
+ end
30
+ end
31
+
32
+ if ActiveRecord.gem_version >= Gem::Version.new("5.0")
33
+ def self.chained_reflection_and_chained_constraints(reflection)
34
+ reflection.chain.map { |ref| [ref, ref.constraints] }.transpose
35
+ end
36
+ else
37
+ def self.chained_reflection_and_chained_constraints(reflection)
38
+ [reflection.chain, reflection.scope_chain]
39
+ end
40
+ end
41
+
42
+ if ActiveRecord.gem_version >= Gem::Version.new("5.0")
43
+ def self.parent_reflection(reflection)
44
+ reflection.parent_reflection
45
+ end
46
+ else
47
+ def self.parent_reflection(reflection)
48
+ _parent_name, parent_refl = reflection.parent_reflection
49
+ parent_refl
50
+ end
51
+ end
52
+
53
+ if ActiveRecord.gem_version >= Gem::Version.new("4.2")
54
+ def self.normalize_association_name(association_name)
55
+ association_name.to_s
56
+ end
57
+ else
58
+ def self.normalize_association_name(association_name)
59
+ association_name.to_sym
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record_compat"
4
+ require_relative "exceptions"
5
+
6
+ module ActiveRecordWhereAssoc
7
+ module CoreLogic
8
+ # Arel table used for aliasing when handling recursive associations (such as parent/children)
9
+ ALIAS_TABLE = Arel::Table.new("_ar_where_assoc_alias_")
10
+
11
+ # Block used when nesting associations for a where_assoc_[not_]exists
12
+ # Will apply the nested scope to the wrapping_scope with: where("EXISTS (SELECT... *nested_scope*)")
13
+ # exists_prefix: raw sql prefix to the EXISTS, ex: 'NOT '
14
+ NestWithExistsBlock = lambda do |wrapping_scope, nested_scope, exists_prefix = ""|
15
+ sql = "#{exists_prefix}EXISTS (#{nested_scope.select('0').to_sql})"
16
+
17
+ wrapping_scope.where(sql)
18
+ end
19
+
20
+ # Block used when nesting associations for a where_assoc_count
21
+ # Will apply the nested scope to the wrapping_scope with: select("SUM(SELECT... *nested_scope*)")
22
+ NestWithSumBlock = lambda do |wrapping_scope, nested_scope|
23
+ # Need the double parentheses
24
+ sql = "SUM((#{nested_scope.to_sql}))"
25
+
26
+ wrapping_scope.unscope(:select).select(sql)
27
+ end
28
+
29
+ # List of available options, used for validation purposes.
30
+ VALID_OPTIONS_KEYS = ActiveRecordWhereAssoc.default_options.keys.freeze
31
+
32
+ def self.validate_options(options)
33
+ invalid_keys = options.keys - VALID_OPTIONS_KEYS
34
+ raise ArgumentError, "Invalid option keys received: #{invalid_keys.join(', ')}" unless invalid_keys.empty?
35
+ end
36
+
37
+ # Gets the value from the options or fallback to default
38
+ def self.option_value(options, key)
39
+ options.fetch(key) { ActiveRecordWhereAssoc.default_options[key] }
40
+ end
41
+
42
+ # Returns a new relation, which is the result of filtering base_relation
43
+ # based on if a record for the specified association of the model exists.
44
+ #
45
+ # See #where_assoc_exists in query_methods.rb for usage details.
46
+ def self.do_where_assoc_exists(base_relation, association_name, given_scope = nil, options = {}, &block)
47
+ nested_relation = relation_on_association(base_relation, association_name, given_scope, options, block, NestWithExistsBlock)
48
+ NestWithExistsBlock.call(base_relation, nested_relation)
49
+ end
50
+
51
+ # Returns a new relation, which is the result of filtering base_relation
52
+ # based on if a record for the specified association of the model doesn't exist.
53
+ #
54
+ # See #where_assoc_exists in query_methods.rb for usage details.
55
+ def self.do_where_assoc_not_exists(base_relation, association_name, given_scope = nil, options = {}, &block)
56
+ nested_relation = relation_on_association(base_relation, association_name, given_scope, options, block, NestWithExistsBlock)
57
+ NestWithExistsBlock.call(base_relation, nested_relation, "NOT ")
58
+ end
59
+
60
+ # Returns a new relation, which is the result of filtering base_relation
61
+ # based on how many records for the specified association of the model exists.
62
+ #
63
+ # See #where_assoc_exists and #where_assoc_count in query_methods.rb for usage details.
64
+ def self.do_where_assoc_count(base_relation, left_operand, operator, association_name, given_scope = nil, options = {}, &block)
65
+ deepest_scope_mod = lambda do |deepest_scope|
66
+ deepest_scope = apply_proc_scope(deepest_scope, block) if block
67
+
68
+ deepest_scope.unscope(:select).select("COUNT(*)")
69
+ end
70
+
71
+ nested_relation = relation_on_association(base_relation, association_name, given_scope, options, deepest_scope_mod, NestWithSumBlock)
72
+ operator = case operator.to_s
73
+ when "=="
74
+ "="
75
+ when "!="
76
+ "<>"
77
+ else
78
+ operator
79
+ end
80
+
81
+ base_relation.where("(#{left_operand}) #{operator} COALESCE((#{nested_relation.to_sql}), 0)")
82
+ end
83
+
84
+ # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
85
+ # association_names_path: can be an array of association names or a single one
86
+ def self.relation_on_association(base_relation, association_names_path, given_scope = nil, options = {},
87
+ last_assoc_block = nil, nest_assocs_block = nil)
88
+ validate_options(options)
89
+ association_names_path = Array.wrap(association_names_path)
90
+
91
+ if association_names_path.size > 1
92
+ recursive_scope_block = lambda do |scope|
93
+ nested_scope = relation_on_association(scope, association_names_path[1..-1], given_scope, options, last_assoc_block, nest_assocs_block)
94
+ nest_assocs_block.call(scope, nested_scope)
95
+ end
96
+
97
+ relation_on_one_association(base_relation, association_names_path.first, nil, options, recursive_scope_block, nest_assocs_block)
98
+ else
99
+ relation_on_one_association(base_relation, association_names_path.first, given_scope, options, last_assoc_block, nest_assocs_block)
100
+ end
101
+ end
102
+
103
+ # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
104
+ def self.relation_on_one_association(base_relation, association_name, given_scope = nil, options = {},
105
+ last_assoc_block = nil, nest_assocs_block = nil)
106
+ relation_klass = base_relation.klass
107
+ final_reflection = fetch_reflection(relation_klass, association_name)
108
+
109
+ nested_scope = nil
110
+ current_scope = nil
111
+
112
+ # Chain deals with through stuff
113
+ # We will start with the reflection that points on the final model, and slowly move back to the reflection
114
+ # that points on the model closest to self
115
+ # Each step, we get all of the scoping lambdas that were defined on associations that apply for
116
+ # the reflection's target
117
+ # Basically, we start from the deepest part of the query and wrap it up
118
+ reflection_chain, constaints_chain = ActiveRecordCompat.chained_reflection_and_chained_constraints(final_reflection)
119
+ skip_next = false
120
+
121
+ reflection_chain.each_with_index do |reflection, i|
122
+ if skip_next
123
+ skip_next = false
124
+ next
125
+ end
126
+
127
+ # the 2nd part of has_and_belongs_to_many is handled at the same time as the first.
128
+ skip_next = true if has_and_belongs_to_many?(reflection)
129
+
130
+ wrapper_scope, current_scope = initial_scope_from_reflection(reflection_chain[i..-1], constaints_chain[i])
131
+
132
+ current_scope = process_association_step_limits(current_scope, reflection, relation_klass, options)
133
+
134
+ if i.zero?
135
+ current_scope = current_scope.where(given_scope) if given_scope
136
+ current_scope = apply_proc_scope(current_scope, last_assoc_block) if last_assoc_block
137
+ end
138
+
139
+ # Those make no sense since we are only limiting the value that would match, using conditions
140
+ current_scope = current_scope.unscope(:limit, :order, :offset)
141
+ current_scope = nest_assocs_block.call(current_scope, nested_scope) if nested_scope
142
+ current_scope = nest_assocs_block.call(wrapper_scope, current_scope) if wrapper_scope
143
+
144
+ nested_scope = current_scope
145
+ end
146
+
147
+ current_scope
148
+ end
149
+
150
+ def self.fetch_reflection(relation_klass, association_name)
151
+ association_name = ActiveRecordCompat.normalize_association_name(association_name)
152
+ reflection = relation_klass._reflections[association_name]
153
+
154
+ if reflection.nil?
155
+ # Need to use build because this exception expects a record...
156
+ raise ActiveRecord::AssociationNotFoundError.new(relation_klass.new, association_name)
157
+ end
158
+ if reflection.macro == :belongs_to && reflection.options[:polymorphic]
159
+ # TODO: We might want an option to indicate that using pluck is ok?
160
+ raise NotImplementedError, "Can't deal with polymorphic belongs_to"
161
+ end
162
+
163
+ reflection
164
+ end
165
+
166
+ def self.initial_scope_from_reflection(reflection_chain, constraints)
167
+ reflection = reflection_chain.first
168
+ current_scope = reflection.klass.default_scoped
169
+
170
+ if has_and_belongs_to_many?(reflection)
171
+ # has_and_belongs_to_many, behind the scene has a secret model and uses a has_many through.
172
+ # This is the first of those two secret has_many through.
173
+ #
174
+ # In order to handle limit, offset, order correctly on has_and_belongs_to_man,
175
+ # we must do both this reflection and the next one at the same time.
176
+ # Think of it this way, if you have limit 3:
177
+ # Apply only on 1st step: You check that any of 2nd step for the first 3 of 1st step match
178
+ # Apply only on 2nd step: You check that any of the first 3 of second step match for any 1st step
179
+ # Apply over both (as we do): You check that only the first 3 of doing both step match,
180
+
181
+ # To create the join, simply using next_reflection.klass.default_scoped.joins(reflection.name)
182
+ # would be great, except we cannot add a given_scope afterward because we are on the wrong "base class",
183
+ # and we can't do #merge because of the LEW crap.
184
+ # So we must do the joins ourself!
185
+ _wrapper, sub_join_contraints = wrapper_and_join_constraints(reflection)
186
+ next_reflection = reflection_chain[1]
187
+
188
+ current_scope = current_scope.joins(<<-SQL)
189
+ INNER JOIN #{next_reflection.klass.quoted_table_name} ON #{sub_join_contraints.to_sql}
190
+ SQL
191
+
192
+ wrapper_scope, join_constaints = wrapper_and_join_constraints(next_reflection, habtm_other_reflection: reflection)
193
+ else
194
+ wrapper_scope, join_constaints = wrapper_and_join_constraints(reflection)
195
+ end
196
+
197
+ constraint_allowed_lim_off = constraint_allowed_lim_off_from(reflection)
198
+
199
+ constraints.each do |callable|
200
+ relation = reflection.klass.unscoped.instance_exec(&callable)
201
+
202
+ if callable != constraint_allowed_lim_off
203
+ # I just want to remove the current values without screwing things in the merge below
204
+ # so we cannot use #unscope
205
+ relation.limit_value = nil
206
+ relation.offset_value = nil
207
+ relation.order_values = []
208
+ end
209
+
210
+ # Need to use merge to replicate the Last Equality Wins behavior of associations
211
+ # https://github.com/rails/rails/issues/7365
212
+ # See also the test/tests/wa_last_equality_wins_test.rb for an explanation
213
+ current_scope = current_scope.merge(relation)
214
+ end
215
+
216
+ [wrapper_scope, current_scope.where(join_constaints)]
217
+ end
218
+
219
+ def self.constraint_allowed_lim_off_from(reflection)
220
+ if has_and_belongs_to_many?(reflection)
221
+ reflection.scope
222
+ else
223
+ # For :through associations, it's pretty hard/tricky to apply limit/offset/order of the
224
+ # whole has_* :through. For now, we only do the direct associations from one model to another
225
+ # that the :through uses and we ignore the limit from the scope of has_* :through.
226
+ #
227
+ # For :through associations, #actual_source_reflection returns final non-through
228
+ # reflection that is reached by following the :source.
229
+ # Otherwise, returns itself.
230
+ reflection.send(:actual_source_reflection).scope
231
+ end
232
+ end
233
+
234
+ def self.process_association_step_limits(current_scope, reflection, relation_klass, options)
235
+ return current_scope.unscope(:limit, :offset, :order) if reflection.macro == :belongs_to
236
+
237
+ current_scope = current_scope.limit(1) if reflection.macro == :has_one
238
+
239
+ current_scope = current_scope.unscope(:limit, :offset) if option_value(options, :ignore_limit)
240
+
241
+ # Order is useless without either limit or offset
242
+ current_scope = current_scope.unscope(:order) if !current_scope.limit_value && !current_scope.offset_value
243
+
244
+ return current_scope unless current_scope.limit_value || current_scope.offset_value
245
+ if %w(mysql mysql2).include?(relation_klass.connection.adapter_name.downcase)
246
+ msg = String.new
247
+ msg << "Associations and default_scopes with a limit or offset are not supported for MySQL (this includes has_many). "
248
+ msg << "Use ignore_limit: true to ignore both limit and offset, and treat has_one like has_many. "
249
+ msg << "See https://github.com/MaxLap/activerecord_where_assoc/tree/ignore_limits#mysql-doesnt-support-sub-limit for details."
250
+ raise MySQLDoesntSupportSubLimitError, msg
251
+ end
252
+
253
+ # We only check the records that would be returned by the associations if called on the model. If:
254
+ # * the association has a limit in its lambda
255
+ # * the default scope of the model has a limit
256
+ # * the association is a has_one
257
+ # Then not every records that match a naive join would be returned. So we first restrict the query to
258
+ # only the records that would be in the range of limit and offset.
259
+ #
260
+ # Note that if the #where_assoc_* block adds a limit or an offset, it has no effect. This is intended.
261
+ # An argument could be made for it to maybe make sense for #where_assoc_count, not sure why that would
262
+ # be useful.
263
+
264
+ if reflection.klass.table_name.include?(".") || option_value(options, :never_alias_limit)
265
+ # This works universally, but seems to sometimes have slower performances.. Need to test if there is an alternative way
266
+ # of expressing this...
267
+ # TODO: Investigate a way to improve performances, or maybe require a flag to do it this way?
268
+ # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless if it
269
+ # could helps the query planner of the DB, if someone can show it to be worth it, then this can be changed.)
270
+
271
+ reflection.klass.unscoped.where(reflection.klass.primary_key.to_sym => current_scope)
272
+ else
273
+ # This works as long as the table_name doesn't have a schema/database, since we need to use an alias
274
+ # with the table name to make scopes and everything else work as expected.
275
+
276
+ # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless if it
277
+ # could helps the query planner of the DB, if someone can show it to be worth it, then this can be changed.)
278
+ reflection.klass.unscoped.from("(#{current_scope.to_sql}) #{reflection.klass.table_name}")
279
+ end
280
+ end
281
+
282
+ # Apply a proc used as scope
283
+ # If it can't receive arguments, call the proc with self set to the relation
284
+ # If it can receive arguments, call the proc the relation passed as argument
285
+ def self.apply_proc_scope(relation, proc_scope)
286
+ if proc_scope.arity == 0
287
+ relation.instance_exec(&proc_scope) || relation
288
+ else
289
+ proc_scope.call(relation) || relation
290
+ end
291
+ end
292
+
293
+ def self.build_wrapper_scope_for_recursive_association(reflection)
294
+ table = reflection.klass.arel_table
295
+ primary_key = reflection.klass.primary_key
296
+ foreign_klass = reflection.send(:actual_source_reflection).active_record
297
+
298
+ wrapper_scope = foreign_klass.base_class.unscoped
299
+ wrapper_scope = wrapper_scope.from("#{table.name} #{ALIAS_TABLE.name}")
300
+ wrapper_scope = wrapper_scope.where(table[primary_key].eq(ALIAS_TABLE[primary_key]))
301
+ wrapper_scope
302
+ end
303
+
304
+ def self.wrapper_and_join_constraints(reflection, options = {})
305
+ join_keys = ActiveRecordCompat.join_keys(reflection)
306
+
307
+ key = join_keys.key
308
+ foreign_key = join_keys.foreign_key
309
+
310
+ table = reflection.klass.arel_table
311
+ foreign_klass = reflection.send(:actual_source_reflection).active_record
312
+ foreign_table = foreign_klass.arel_table
313
+
314
+ habtm_other_reflection = options[:habtm_other_reflection]
315
+ habtm_other_table = habtm_other_reflection.klass.arel_table if habtm_other_reflection
316
+
317
+ if (habtm_other_table || table).name == foreign_table.name
318
+ wrapper_scope = build_wrapper_scope_for_recursive_association(habtm_other_reflection || reflection)
319
+ foreign_table = ALIAS_TABLE
320
+ end
321
+
322
+ constraints = table[key].eq(foreign_table[foreign_key])
323
+
324
+ if reflection.type
325
+ # Handing of the polymorphic has_many/has_one's type column
326
+ constraints = constraints.and(table[reflection.type].eq(foreign_klass.base_class.name))
327
+ end
328
+
329
+ [wrapper_scope, constraints]
330
+ end
331
+
332
+ def self.has_and_belongs_to_many?(reflection) # rubocop:disable Naming/PredicateName
333
+ parent = ActiveRecordCompat.parent_reflection(reflection)
334
+ parent && parent.macro == :has_and_belongs_to_many
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordWhereAssoc
4
+ class MySQLDoesntSupportSubLimitError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,177 @@
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, spam_flag: true)
59
+ #
60
+ # # Using a String
61
+ # Post.where_assoc_exists(:comments, "spam_flag = true")
62
+ #
63
+ # # Using an Array (a string and its binds)
64
+ # Post.where_assoc_exists(:comments, ["spam_flag = ?", 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(spam_flag: 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(spam_flag: 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: a number or any string of SQL to embed in the query used for the left
154
+ # operand of the comparison.
155
+ # 2nd argument: the operator to use: :<, :<=, :==, :!=, :>=, :>
156
+ # 3rd, 4th and 5th arguments: same as #where_assoc_exists' 1st, 2nd and 3rd arguments
157
+ # block: same as #where_assoc_exists' block
158
+ #
159
+ # The order of the parameters may seem confusing. But you will get used to it. To help
160
+ # remember the order of the parameters, remember that the goal is to do:
161
+ # 5 < (SELECT COUNT(*) FROM ...)
162
+ # So the parameters are in the same order as in that query: number, operator, association.
163
+ #
164
+ # To be clear, when you use multiple associations in an array, the count you will be
165
+ # comparing against is the total number of records of that last association.
166
+ #
167
+ # # The users that have received at least 5 comments total on all of their posts
168
+ # # So this can be one post that has 5 comments of 5 posts with 1 comments
169
+ # User.where_assoc_count(5, :<=, [:posts, :comments])
170
+ #
171
+ # # The users that have at least 5 posts with at least one comments
172
+ # User.where_assoc_count(5, :<=, :posts) { where_assoc_exists(:comments) }
173
+ def where_assoc_count(left_operand, operator, association_name, given_scope = nil, options = {}, &block)
174
+ ActiveRecordWhereAssoc::CoreLogic.do_where_assoc_count(self, left_operand, operator, association_name, given_scope, options, &block)
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Needed for delegate
4
+ require "active_support"
5
+
6
+ module ActiveRecordWhereAssoc
7
+ module Querying
8
+ new_query_methods = QueryMethods.public_instance_methods
9
+ delegate(*new_query_methods, to: :all)
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordWhereAssoc
4
+ VERSION = "0.1.0".freeze
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Just in case of typo in the require. Call the right one automatically
4
+
5
+
6
+ require_relative "active_record_where_assoc"
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord_where_assoc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maxime Handfield Lapointe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.15'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.15'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
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
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.54.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.54.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: sqlite3
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: 'Adds various #where_assoc_* methods to ActiveRecord to make it easy
140
+ to do correct conditions on the associations of the model being queried.'
141
+ email:
142
+ - maxhlap@gmail.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - LICENSE.txt
148
+ - README.md
149
+ - lib/active_record_where_assoc.rb
150
+ - lib/active_record_where_assoc/active_record_compat.rb
151
+ - lib/active_record_where_assoc/core_logic.rb
152
+ - lib/active_record_where_assoc/exceptions.rb
153
+ - lib/active_record_where_assoc/query_methods.rb
154
+ - lib/active_record_where_assoc/querying.rb
155
+ - lib/active_record_where_assoc/version.rb
156
+ - lib/activerecord_where_assoc.rb
157
+ homepage: https://github.com/MaxLap/activerecord_where_assoc
158
+ licenses:
159
+ - MIT
160
+ metadata: {}
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubyforge_project:
177
+ rubygems_version: 2.6.11
178
+ signing_key:
179
+ specification_version: 4
180
+ summary: Make ActiveRecord do conditions on your associations
181
+ test_files: []