activerecord_where_assoc 0.1.2 → 0.1.3

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: 78be7fdd6b3da54253f5fe51709c258256dbe013341b69c3f0c9a23a3e2968f3
4
- data.tar.gz: 69c40eabe1332c945abef850347d2b39dc4412eedc65cc02aabcd07901ef2de5
3
+ metadata.gz: '0805c22b1a2c499a1d642f4bf4c624e31318192e2243a38ef3dca92cadce2a58'
4
+ data.tar.gz: bbebf2d6e4402c07a54af54fa87c25c5f94c7813e4b0cc6cc63a761bd291cb7c
5
5
  SHA512:
6
- metadata.gz: f004ca618191fe6b8841d354ef10b3e0209e6721f5d35d5ebc6aff87f13fcbf62a92e0cd42bd77ef4b87b987310a56155bab9f50b59915b361871c3d51cebe4a
7
- data.tar.gz: 84da6aaedd53c494913bad518a36ca18dd717c3ac2d3dbed3af5fe78561716ad0c38af67832272ea2b26dddd0319f5ae820a1d636cea5fb14aa27a8814a2f2dc
6
+ metadata.gz: b17588d07e22a79132e62f6341fad4b4e39106ec2b957bed866d85ae3e20c59de95efae7ea1a91cdd812111c40afda385277800a90fc33b95ec14ad5af9f9bdb
7
+ data.tar.gz: b34651a306ac5503fa96bf30058c00e1b071f9bcb5bc429b249c7455a2d8ec6d6fe6518c95c6060aa507f3fb59bfe44434aba4c5fc0e1ffec6b25c673209e13f
@@ -0,0 +1,14 @@
1
+
2
+ * Use `SELECT 1` instead of `SELECT 0`...
3
+ ... it just seems more natural that way.
4
+ * Bugfixes
5
+
6
+ # 0.1.2
7
+
8
+ * It is now possible to pass a `Range` as first argument to `#where_assoc_count`.
9
+ Ex: Users that have between 10 and 20 posts
10
+ `User.where_assoc_count(10..20, :==, :posts)`
11
+ The operator in that case must be either :== or :!=.
12
+ This will use `BETWEEN` and `NOT BETWEEN`.
13
+ Ranges that exclude the last value, i.e. `5...10`, are also supported, resulting in `BETWEEN 5 and 9`.
14
+ Ranges with infinities are also supported.
@@ -2,7 +2,7 @@ Here are some example usages of the gem, along with the generated SQL. Each of t
2
2
 
3
3
  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
4
 
5
- The content below is generated from running `ruby examples/examples.rb`
5
+ The content of this file is generated from running `ruby examples/examples.rb`
6
6
 
7
7
  -------
8
8
 
@@ -13,9 +13,9 @@ The content below is generated from running `ruby examples/examples.rb`
13
13
  Post.where_assoc_exists(:comments)
14
14
  ```
15
15
  ```sql
16
- SELECT "posts".* FROM "posts"
16
+ SELECT "posts".* FROM "posts"
17
17
  WHERE (EXISTS (
18
- SELECT 0 FROM "comments"
18
+ SELECT 1 FROM "comments"
19
19
  WHERE "comments"."post_id" = "posts"."id"
20
20
  ))
21
21
  ```
@@ -27,9 +27,9 @@ SELECT "posts".* FROM "posts"
27
27
  Post.where_assoc_not_exists(:comments)
28
28
  ```
29
29
  ```sql
30
- SELECT "posts".* FROM "posts"
30
+ SELECT "posts".* FROM "posts"
31
31
  WHERE (NOT EXISTS (
32
- SELECT 0 FROM "comments"
32
+ SELECT 1 FROM "comments"
33
33
  WHERE "comments"."post_id" = "posts"."id"
34
34
  ))
35
35
  ```
@@ -41,9 +41,9 @@ SELECT "posts".* FROM "posts"
41
41
  Post.where_assoc_count(50, :<=, :comments)
42
42
  ```
43
43
  ```sql
44
- SELECT "posts".* FROM "posts"
44
+ SELECT "posts".* FROM "posts"
45
45
  WHERE ((50) <= COALESCE((
46
- SELECT COUNT(*) FROM "comments"
46
+ SELECT COUNT(*) FROM "comments"
47
47
  WHERE "comments"."post_id" = "posts"."id"
48
48
  ), 0))
49
49
  ```
@@ -57,9 +57,9 @@ SELECT "posts".* FROM "posts"
57
57
  my_post.comments.where_assoc_exists(:author, is_admin: true)
58
58
  ```
59
59
  ```sql
60
- SELECT "comments".* FROM "comments"
60
+ SELECT "comments".* FROM "comments"
61
61
  WHERE "comments"."post_id" = 1 AND (EXISTS (
62
- SELECT 0 FROM "users"
62
+ SELECT 1 FROM "users"
63
63
  WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
64
64
  ))
65
65
  ```
@@ -71,9 +71,9 @@ SELECT "comments".* FROM "comments"
71
71
  my_post.comments.where_assoc_not_exists(:author, &:admins)
72
72
  ```
73
73
  ```sql
74
- SELECT "comments".* FROM "comments"
74
+ SELECT "comments".* FROM "comments"
75
75
  WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
76
- SELECT 0 FROM "users"
76
+ SELECT 1 FROM "users"
77
77
  WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
78
78
  ))
79
79
  ```
@@ -85,9 +85,9 @@ SELECT "comments".* FROM "comments"
85
85
  Post.where_assoc_count(5, :<=, :comments, ["is_reported = ?", true])
86
86
  ```
87
87
  ```sql
88
- SELECT "posts".* FROM "posts"
88
+ SELECT "posts".* FROM "posts"
89
89
  WHERE ((5) <= COALESCE((
90
- SELECT COUNT(*) FROM "comments"
90
+ SELECT COUNT(*) FROM "comments"
91
91
  WHERE "comments"."post_id" = "posts"."id" AND (is_reported = 't')
92
92
  ), 0))
93
93
  ```
@@ -99,9 +99,9 @@ SELECT "posts".* FROM "posts"
99
99
  Post.where_assoc_exists(:author, "is_admin = 't'")
100
100
  ```
101
101
  ```sql
102
- SELECT "posts".* FROM "posts"
102
+ SELECT "posts".* FROM "posts"
103
103
  WHERE (EXISTS (
104
- SELECT 0 FROM "users"
104
+ SELECT 1 FROM "users"
105
105
  WHERE "users"."id" = "posts"."author_id" AND (is_admin = 't')
106
106
  ))
107
107
  ```
@@ -113,9 +113,9 @@ SELECT "posts".* FROM "posts"
113
113
  my_post.comments.where_assoc_not_exists(:author) { admins }
114
114
  ```
115
115
  ```sql
116
- SELECT "comments".* FROM "comments"
116
+ SELECT "comments".* FROM "comments"
117
117
  WHERE "comments"."post_id" = 1 AND (NOT EXISTS (
118
- SELECT 0 FROM "users"
118
+ SELECT 1 FROM "users"
119
119
  WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
120
120
  ))
121
121
  ```
@@ -123,15 +123,15 @@ SELECT "comments".* FROM "comments"
123
123
  ---
124
124
 
125
125
  ```ruby
126
- # Posts that have at least 5 reported comments (Using block with #where)
127
- Post.where_assoc_count(5, :<=, :comments) { where(is_reported: true) }
126
+ # Posts that have 5 to 10 reported comments (Using block with #where and range for count)
127
+ Post.where_assoc_count(5..10, :==, :comments) { where(is_reported: true) }
128
128
  ```
129
129
  ```sql
130
- SELECT "posts".* FROM "posts"
131
- WHERE ((5) <= COALESCE((
132
- SELECT COUNT(*) FROM "comments"
130
+ SELECT "posts".* FROM "posts"
131
+ WHERE (COALESCE((
132
+ SELECT COUNT(*) FROM "comments"
133
133
  WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't'
134
- ), 0))
134
+ ), 0) BETWEEN 5 AND 10)
135
135
  ```
136
136
 
137
137
  ---
@@ -141,9 +141,9 @@ SELECT "posts".* FROM "posts"
141
141
  Comment.where_assoc_exists(:post, author_id: my_user.id)
142
142
  ```
143
143
  ```sql
144
- SELECT "comments".* FROM "comments"
144
+ SELECT "comments".* FROM "comments"
145
145
  WHERE (EXISTS (
146
- SELECT 0 FROM "posts"
146
+ SELECT 1 FROM "posts"
147
147
  WHERE "posts"."id" = "comments"."post_id" AND "posts"."author_id" = 1
148
148
  ))
149
149
  ```
@@ -157,11 +157,11 @@ SELECT "comments".* FROM "comments"
157
157
  Post.where_assoc_exists([:comments, :author], is_admin: true)
158
158
  ```
159
159
  ```sql
160
- SELECT "posts".* FROM "posts"
160
+ SELECT "posts".* FROM "posts"
161
161
  WHERE (EXISTS (
162
- SELECT 0 FROM "comments"
162
+ SELECT 1 FROM "comments"
163
163
  WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
164
- SELECT 0 FROM "users"
164
+ SELECT 1 FROM "users"
165
165
  WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
166
166
  ))
167
167
  ))
@@ -174,9 +174,9 @@ SELECT "posts".* FROM "posts"
174
174
  Post.where_assoc_exists(:comments, "posts.author_id = comments.author_id")
175
175
  ```
176
176
  ```sql
177
- SELECT "posts".* FROM "posts"
177
+ SELECT "posts".* FROM "posts"
178
178
  WHERE (EXISTS (
179
- SELECT 0 FROM "comments"
179
+ SELECT 1 FROM "comments"
180
180
  WHERE "comments"."post_id" = "posts"."id" AND (posts.author_id = comments.author_id)
181
181
  ))
182
182
  ```
@@ -190,11 +190,11 @@ Post.where_assoc_exists(:comments, is_reported: true) {
190
190
  }
191
191
  ```
192
192
  ```sql
193
- SELECT "posts".* FROM "posts"
193
+ SELECT "posts".* FROM "posts"
194
194
  WHERE (EXISTS (
195
- SELECT 0 FROM "comments"
195
+ SELECT 1 FROM "comments"
196
196
  WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't' AND (EXISTS (
197
- SELECT 0 FROM "users"
197
+ SELECT 1 FROM "users"
198
198
  WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
199
199
  ))
200
200
  ))
@@ -208,14 +208,14 @@ my_user.posts.where_assoc_exists(:comments, is_reported: true)
208
208
  .where_assoc_exists([:comments, :author], is_admin: true)
209
209
  ```
210
210
  ```sql
211
- SELECT "posts".* FROM "posts"
211
+ SELECT "posts".* FROM "posts"
212
212
  WHERE "posts"."author_id" = 1 AND (EXISTS (
213
- SELECT 0 FROM "comments"
213
+ SELECT 1 FROM "comments"
214
214
  WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 't'
215
215
  )) AND (EXISTS (
216
- SELECT 0 FROM "comments"
216
+ SELECT 1 FROM "comments"
217
217
  WHERE "comments"."post_id" = "posts"."id" AND (EXISTS (
218
- SELECT 0 FROM "users"
218
+ SELECT 1 FROM "users"
219
219
  WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 't'
220
220
  ))
221
221
  ))
data/README.md CHANGED
@@ -35,9 +35,9 @@ This gem is very new. If you have any feedback, good or bad, do not hesitate to
35
35
  * Suggestions to make the documentation easier to follow / more complete.
36
36
 
37
37
 
38
- ## 0.1.0
38
+ ## 0.1
39
39
 
40
- 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.
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.
41
41
 
42
42
  ## Installation
43
43
 
@@ -12,7 +12,7 @@ module ActiveRecordWhereAssoc
12
12
  # Will apply the nested scope to the wrapping_scope with: where("EXISTS (SELECT... *nested_scope*)")
13
13
  # exists_prefix: raw sql prefix to the EXISTS, ex: 'NOT '
14
14
  NestWithExistsBlock = lambda do |wrapping_scope, nested_scope, exists_prefix = ""|
15
- sql = "#{exists_prefix}EXISTS (#{nested_scope.select('0').to_sql})"
15
+ sql = "#{exists_prefix}EXISTS (#{nested_scope.select('1').to_sql})"
16
16
 
17
17
  wrapping_scope.where(sql)
18
18
  end
@@ -43,7 +43,7 @@ module ActiveRecordWhereAssoc
43
43
  # based on if a record for the specified association of the model exists.
44
44
  #
45
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)
46
+ def self.do_where_assoc_exists(base_relation, association_name, given_scope, options, &block)
47
47
  nested_relation = relation_on_association(base_relation, association_name, given_scope, options, block, NestWithExistsBlock)
48
48
  NestWithExistsBlock.call(base_relation, nested_relation)
49
49
  end
@@ -52,7 +52,7 @@ module ActiveRecordWhereAssoc
52
52
  # based on if a record for the specified association of the model doesn't exist.
53
53
  #
54
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)
55
+ def self.do_where_assoc_not_exists(base_relation, association_name, given_scope, options, &block)
56
56
  nested_relation = relation_on_association(base_relation, association_name, given_scope, options, block, NestWithExistsBlock)
57
57
  NestWithExistsBlock.call(base_relation, nested_relation, "NOT ")
58
58
  end
@@ -61,7 +61,7 @@ module ActiveRecordWhereAssoc
61
61
  # based on how many records for the specified association of the model exists.
62
62
  #
63
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)
64
+ def self.do_where_assoc_count(base_relation, left_operand, operator, association_name, given_scope, options, &block)
65
65
  deepest_scope_mod = lambda do |deepest_scope|
66
66
  deepest_scope = apply_proc_scope(deepest_scope, block) if block
67
67
 
@@ -76,8 +76,7 @@ module ActiveRecordWhereAssoc
76
76
 
77
77
  # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
78
78
  # association_names_path: can be an array of association names or a single one
79
- def self.relation_on_association(base_relation, association_names_path, given_scope = nil, options = {},
80
- last_assoc_block = nil, nest_assocs_block = nil)
79
+ def self.relation_on_association(base_relation, association_names_path, given_scope, options, last_assoc_block, nest_assocs_block)
81
80
  validate_options(options)
82
81
  association_names_path = Array.wrap(association_names_path)
83
82
 
@@ -94,8 +93,7 @@ module ActiveRecordWhereAssoc
94
93
  end
95
94
 
96
95
  # Returns the receiver (with possible alterations) and a relation meant to be embed in the received.
97
- def self.relation_on_one_association(base_relation, association_name, given_scope = nil, options = {},
98
- last_assoc_block = nil, nest_assocs_block = nil)
96
+ def self.relation_on_one_association(base_relation, association_name, given_scope, options, last_assoc_block, nest_assocs_block)
99
97
  relation_klass = base_relation.klass
100
98
  final_reflection = fetch_reflection(relation_klass, association_name)
101
99
 
@@ -118,7 +116,7 @@ module ActiveRecordWhereAssoc
118
116
  end
119
117
 
120
118
  # the 2nd part of has_and_belongs_to_many is handled at the same time as the first.
121
- skip_next = true if has_and_belongs_to_many?(reflection)
119
+ skip_next = true if actually_has_and_belongs_to_many?(reflection)
122
120
 
123
121
  wrapper_scope, current_scope = initial_scope_from_reflection(reflection_chain[i..-1], constaints_chain[i])
124
122
 
@@ -160,11 +158,11 @@ module ActiveRecordWhereAssoc
160
158
  reflection = reflection_chain.first
161
159
  current_scope = reflection.klass.default_scoped
162
160
 
163
- if has_and_belongs_to_many?(reflection)
161
+ if actually_has_and_belongs_to_many?(reflection)
164
162
  # has_and_belongs_to_many, behind the scene has a secret model and uses a has_many through.
165
163
  # This is the first of those two secret has_many through.
166
164
  #
167
- # In order to handle limit, offset, order correctly on has_and_belongs_to_man,
165
+ # In order to handle limit, offset, order correctly on has_and_belongs_to_many,
168
166
  # we must do both this reflection and the next one at the same time.
169
167
  # Think of it this way, if you have limit 3:
170
168
  # Apply only on 1st step: You check that any of 2nd step for the first 3 of 1st step match
@@ -210,22 +208,19 @@ module ActiveRecordWhereAssoc
210
208
  end
211
209
 
212
210
  def self.constraint_allowed_lim_off_from(reflection)
213
- if has_and_belongs_to_many?(reflection)
214
- reflection.scope
215
- else
216
- # For :through associations, it's pretty hard/tricky to apply limit/offset/order of the
217
- # whole has_* :through. For now, we only do the direct associations from one model to another
218
- # that the :through uses and we ignore the limit from the scope of has_* :through.
219
- #
220
- # For :through associations, #actual_source_reflection returns final non-through
221
- # reflection that is reached by following the :source.
222
- # Otherwise, returns itself.
223
- reflection.send(:actual_source_reflection).scope
224
- end
211
+ # For :through associations, it's pretty hard/tricky to apply limit/offset/order of the
212
+ # whole has_* :through. For now, we only apply those of the direct associations from one model
213
+ # to another that the :through uses and we ignore the limit/offset/order from the scope of has_* :through.
214
+ #
215
+ # The exception is for has_and_belongs_to_many, which behind the scene, use a has_many :through.
216
+ # For those, since we know there is no limits on the internal has_many and the belongs_to,
217
+ # we can do a special case and handle their limit. This way, we can treat them the same way we treat
218
+ # the other macros, we only apply the limit/offset/order of the deepest user-define association.
219
+ user_defined_actual_source_reflection(reflection).scope
225
220
  end
226
221
 
227
222
  def self.process_association_step_limits(current_scope, reflection, relation_klass, options)
228
- return current_scope.unscope(:limit, :offset, :order) if reflection.macro == :belongs_to
223
+ return current_scope.unscope(:limit, :offset, :order) if user_defined_actual_source_reflection(reflection).macro == :belongs_to
229
224
 
230
225
  current_scope = current_scope.limit(1) if reflection.macro == :has_one
231
226
 
@@ -327,6 +322,23 @@ module ActiveRecordWhereAssoc
327
322
  parent && parent.macro == :has_and_belongs_to_many
328
323
  end
329
324
 
325
+ # Return true if #user_defined_actual_source_reflection is a has_and_belongs_to_many
326
+ def self.actually_has_and_belongs_to_many?(reflection)
327
+ has_and_belongs_to_many?(user_defined_actual_source_reflection(reflection))
328
+ end
329
+
330
+ # Returns the deepest user-defined reflection using source_reflection.
331
+ # This is different from #send(:actual_source_reflection) because it stops on
332
+ # has_and_belongs_to_many associations, where as actual_source_reflection would continue
333
+ # down to the belongs_to that is used internally.
334
+ def self.user_defined_actual_source_reflection(reflection)
335
+ loop do
336
+ return reflection if reflection == reflection.source_reflection
337
+ return reflection if has_and_belongs_to_many?(reflection)
338
+ reflection = reflection.source_reflection
339
+ end
340
+ end
341
+
330
342
  # Doing (SQL) BETWEEN v1 AND v2, where v2 is infinite means (SQL) >= v1. However,
331
343
  # we place the SQL on the right side, so the operator is flipped to become v1 <= (SQL).
332
344
  # Doing (SQL) NOT BETWEEN v1 AND v2 where v2 is infinite means (SQL) < v1. However,
@@ -353,7 +365,7 @@ module ActiveRecordWhereAssoc
353
365
  raise ArgumentError, "Operator should be one of '==', '=', '<>' or '!=' when using a Range not: #{operator.inspect}"
354
366
  end
355
367
 
356
- v1 = left_operand.begin || 0
368
+ v1 = left_operand.begin
357
369
  v2 = left_operand.end || Float::INFINITY
358
370
 
359
371
  v1 = 0 if v1 == -Float::INFINITY
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordWhereAssoc
4
- VERSION = "0.1.2".freeze
4
+ VERSION = "0.1.3".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.2
4
+ version: 0.1.3
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-10 00:00:00.000000000 Z
11
+ date: 2018-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: deep-cover
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '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'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rubocop
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -126,16 +140,16 @@ dependencies:
126
140
  name: niceql
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
- - - '='
143
+ - - ">="
130
144
  - !ruby/object:Gem::Version
131
- version: 0.1.14
145
+ version: 0.1.23
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
- - - '='
150
+ - - ">="
137
151
  - !ruby/object:Gem::Version
138
- version: 0.1.14
152
+ version: 0.1.23
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: sqlite3
141
155
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +173,7 @@ extensions: []
159
173
  extra_rdoc_files: []
160
174
  files:
161
175
  - ALTERNATIVES_PROBLEMS.md
176
+ - CHANGELOG.md
162
177
  - EXAMPLES.md
163
178
  - LICENSE.txt
164
179
  - README.md