where_exists 1.2.1 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10aa100274a4f1cbc5ffd1efddaf255242a4ffa99bb4aa857ec807e4060a2198
4
- data.tar.gz: 7572426b34fd9056a781006dce704de782bc94f48899ec66d6cdb2ce7fd00aab
3
+ metadata.gz: d9957b5787ab2fda2505560b01ffeca8f2e1d24bf0d0d38e09f4f25969a30ab6
4
+ data.tar.gz: 4718917be7477f54afb0e322a588adadb4d2d375c6b7938d922b89ef0629ea27
5
5
  SHA512:
6
- metadata.gz: d17add3cb16b2edeca33761dd8097b4da1f1203af9ffe891b0c2756d6e606b08b12ac7ce67bd8e5869cfee6389fbfc8fcb26da2d00941d00ab9c00d6691c8af9
7
- data.tar.gz: 33f4b910af735aedb797329d99c876a5978fdaac103f704044ad9095e4856e4cffc17359f5f2ad0c4f21379483622f25fecd842a4928327e2641d21078cee77c
6
+ metadata.gz: 839de9969500c67df98512ee5f5043b4d34a8e337263e3dcc79dd081e6730a0b1bb6bce3c632a764372d6dabe143f9b3c7ffcbc55092f55727d8746d40eafaf3
7
+ data.tar.gz: a4c38490f6caf945889d2f92075e1069cef92706524c299bd4d800c4b856fd62fda39738539fee816daa955a5ef3908e49c7aed9ed4e3be97af6bf8c419f3b07
data/README.markdown ADDED
@@ -0,0 +1,174 @@
1
+ # Where Exists
2
+ **Rails way to harness the power of SQL EXISTS condition**<br>
3
+ [![Gem Version](https://badge.fury.io/rb/where_exists.svg)](http://badge.fury.io/rb/where_exists)
4
+
5
+ ## Description
6
+
7
+ <img src="exists.png" alt="Exists" align="right" width="100" height="200">
8
+
9
+ This gem does exactly two things:
10
+
11
+ * Selects each model object for which there is a certain associated object
12
+ * Selects each model object for which there aren't any certain associated objects
13
+
14
+ It uses SQL [EXISTS condition](http://www.techonthenet.com/sql/exists.php) to do it fast, and extends ActiveRecord with `where_exists` and `where_not_exists` methods to make its usage simple and straightforward.
15
+
16
+ ## Quick start
17
+
18
+ Add gem to Gemfile:
19
+
20
+ gem 'where_exists'
21
+
22
+ and run `bundle install` as usual.
23
+
24
+ And now you have `where_exists` and `where_not_exists` methods available for your ActiveRecord models and relations.
25
+
26
+ Syntax:
27
+
28
+ ```ruby
29
+ Model.where_exists(association, additional_finder_parameters)
30
+ ```
31
+
32
+ Supported Rails versions: >= 5.2.
33
+
34
+ ## Example of usage
35
+
36
+ Given there is User model:
37
+
38
+ ```ruby
39
+ class User < ActiveRecord::Base
40
+ has_many :connections
41
+ has_many :groups, through: :connections
42
+ end
43
+ ```
44
+
45
+ And Group:
46
+
47
+ ```ruby
48
+ class Group < ActiveRecord::Base
49
+ has_many :connections
50
+ has_many :users, through: :connections
51
+ end
52
+ ```
53
+
54
+ And standard many-to-many Connection:
55
+
56
+ ```ruby
57
+ class Connection
58
+ belongs_to :user
59
+ belongs_to :group
60
+ end
61
+ ```
62
+
63
+ What I want to do is to:
64
+
65
+ * Select users who don't belong to given set of Groups (groups with ids `[4,5,6]`)
66
+ * Select users who belong to one set of Groups (`[1,2,3]`) and don't belong to another (`[4,5,6]`)
67
+ * Select users who don't belong to a Group
68
+
69
+ Also, I don't want to:
70
+
71
+ * Fetch a lot of data from database to manipulate it with Ruby code. I know that will be inefficient in terms of CPU and memory (Ruby is much slower than any commonly used DB engine, and typically I want to rely on DB engine to do the heavy lifting)
72
+ * I tried queries like `User.joins(:group).where(group_id: [1,2,3]).where.not(group_id: [4,5,6])` and they return wrong results (some users from the result set belong to groups 4,5,6 *as well as* 1,2,3)
73
+ * I don't want to do `join` merely for the sake of only checking for existence, because I know that that is a pretty complex (i.e. CPU/memory-intensive) operation for DB
74
+
75
+ <sub><sup>If you wonder how to do that without the gem (i.e. essentially by writing SQL EXISTS statement manually) see that [StackOverflow answer](http://stackoverflow.com/a/32016347/5029266) (disclosure: it's self-answered question of a contributor of this gem).</sup></sub>
76
+
77
+ And now you are able to do all these things (and more) as simple as:
78
+
79
+ > Select only users who don't belong to given set of Groups (groups with ids `[4,5,6]`)
80
+
81
+ ```ruby
82
+ # It's really neat, isn't it?
83
+ User.where_exists(:groups, id: [4,5,6])
84
+ ```
85
+
86
+ <sub><sup>Notice that the second argument is `where` parameters for Group model</sup></sub>
87
+
88
+ > Select only users who belong to one set of Groups (`[1,2,3]`) and don't belong to another (`[4,5,6]`)
89
+
90
+ ```ruby
91
+ # Chain-able like you expect them to be.
92
+ #
93
+ # Additional finder parameters is anything that
94
+ # could be fed to 'where' method.
95
+ #
96
+ # Let's use 'name' instead of 'id' here, for example.
97
+
98
+ User.where_exists(:groups, name: ['first','second','third']).
99
+ where_not_exists(:groups, name: ['fourth','fifth','sixth'])
100
+ ```
101
+
102
+ <sub><sup>It is possible to add as much attributes to the criteria as it is necessary, just as with regular `where(...)`</sub></sup>
103
+
104
+ > Select only users who don't belong to a Group
105
+
106
+ ```ruby
107
+ # And that's just its basic capabilities
108
+ User.where_not_exists(:groups)
109
+ ```
110
+
111
+ <sub><sup>Adding parameters (the second argument) to `where_not_exists` method is feasible as well, if you have such requirements.</sup></sub>
112
+
113
+
114
+ > Re-use existing scopes
115
+
116
+ ```ruby
117
+ User.where_exists(:groups) do |groups_scope|
118
+ groups_scope.activated_since(Time.now)
119
+ end
120
+
121
+ User.where_exists(:groups, &:approved)
122
+ ```
123
+ <sub><sup>If you pass a block to `where_exists`, the scope of the relation will be yielded to your block so you can re-use existing scopes.</sup></sub>
124
+
125
+
126
+
127
+ ## Additional capabilities
128
+
129
+ **Q**: Does it support both `has_many` and `belongs_to` association type?<br>
130
+ **A**: Yes.
131
+
132
+
133
+ **Q**: Does it support polymorphic associations?<br>
134
+ **A**: Yes, both ways.
135
+
136
+
137
+ **Q**: Does it support multi-level (recursive) `:through` associations?<br>
138
+ **A**: You bet. (Now you can forget complex EXISTS or JOIN statetements in a pretty wide variety of similar cases.)
139
+
140
+
141
+ **Q**: Does it support `where` parameters with interpolation, e.g. `parent.where_exists(:child, 'fieldA > ?', 1)`?<br>
142
+ **A**: Yes.
143
+
144
+
145
+ **Q**: Does it take into account default association condition, e.g. `has_many :drafts, -> { where published: nil }`?<br>
146
+ **A**: Yes.
147
+
148
+ ## Contributing
149
+
150
+ If you find that this gem lacks certain possibilities that you would have found useful, don't hesitate to create a [feature request](https://github.com/EugZol/where_exists/issues).
151
+
152
+ Also,
153
+
154
+ * Report bugs
155
+ * Submit pull request with new features or bug fixes
156
+ * Enhance or clarify the documentation that you are reading
157
+
158
+ **Please ping me in addition to creating PR/issue** (just add "@EugZol" to the PR/issue text). Thank you!
159
+
160
+ To run tests:
161
+ ```
162
+ > bundle exec appraisal install
163
+ > bundle exec appraisal rake test
164
+ ```
165
+
166
+ ## License
167
+
168
+ This project uses MIT license. See [`MIT-LICENSE`](https://github.com/EugZol/where_exists/blob/master/MIT-LICENSE) file for full text.
169
+
170
+ ## Alternatives
171
+
172
+ One known alternative is https://github.com/MaxLap/activerecord_where_assoc
173
+
174
+ A comprehensive comparison is made by MaxLap here: https://github.com/MaxLap/activerecord_where_assoc/blob/master/ALTERNATIVES_PROBLEMS.md
@@ -1,3 +1,3 @@
1
1
  module WhereExists
2
- VERSION = "1.2.1"
2
+ VERSION = "2.0.2"
3
3
  end
data/lib/where_exists.rb CHANGED
@@ -20,7 +20,11 @@ module WhereExists
20
20
  not_string = "NOT "
21
21
  end
22
22
 
23
- self.where("#{not_string}(#{queries_sql})")
23
+ if queries_sql.empty?
24
+ does_exist ? self.none : self.all
25
+ else
26
+ self.where("#{not_string}(#{queries_sql})")
27
+ end
24
28
  end
25
29
 
26
30
  def build_exists_string(association_name, *where_parameters, &block)
@@ -32,11 +36,11 @@ module WhereExists
32
36
 
33
37
  case association.macro
34
38
  when :belongs_to
35
- queries = where_exists_for_belongs_to_query(association, where_parameters)
39
+ queries = where_exists_for_belongs_to_query(association, where_parameters, &block)
36
40
  when :has_many, :has_one
37
- queries = where_exists_for_has_many_query(association, where_parameters)
41
+ queries = where_exists_for_has_many_query(association, where_parameters, &block)
38
42
  when :has_and_belongs_to_many
39
- queries = where_exists_for_habtm_query(association, where_parameters)
43
+ queries = where_exists_for_habtm_query(association, where_parameters, &block)
40
44
  else
41
45
  inspection = nil
42
46
  begin
@@ -49,13 +53,12 @@ module WhereExists
49
53
 
50
54
  queries_sql =
51
55
  queries.map do |query|
52
- query = yield query if block_given?
53
56
  "EXISTS (" + query.to_sql + ")"
54
57
  end
55
58
  queries_sql.join(" OR ")
56
59
  end
57
60
 
58
- def where_exists_for_belongs_to_query(association, where_parameters)
61
+ def where_exists_for_belongs_to_query(association, where_parameters, &block)
59
62
  polymorphic = association.options[:polymorphic].present?
60
63
 
61
64
  association_scope = association.scope
@@ -89,13 +92,14 @@ module WhereExists
89
92
 
90
93
  query = query.where("#{self_type} IN (?)", other_types.uniq)
91
94
  end
95
+ query = yield query if block_given?
92
96
  queries.push query
93
97
  end
94
98
 
95
99
  queries
96
100
  end
97
101
 
98
- def where_exists_for_has_many_query(association, where_parameters, next_association = {})
102
+ def where_exists_for_has_many_query(association, where_parameters, next_association = {}, &block)
99
103
  if association.through_reflection
100
104
  raise ArgumentError.new(association) unless association.source_reflection
101
105
  next_association = {
@@ -107,9 +111,9 @@ module WhereExists
107
111
 
108
112
  case association.macro
109
113
  when :has_many, :has_one
110
- return where_exists_for_has_many_query(association, {}, next_association)
114
+ return where_exists_for_has_many_query(association, {}, next_association, &block)
111
115
  when :has_and_belongs_to_many
112
- return where_exists_for_habtm_query(association, {}, next_association)
116
+ return where_exists_for_habtm_query(association, {}, next_association, &block)
113
117
  else
114
118
  inspection = nil
115
119
  begin
@@ -144,17 +148,18 @@ module WhereExists
144
148
  end
145
149
 
146
150
  if next_association[:association]
147
- return loop_nested_association(result, next_association)
151
+ return loop_nested_association(result, next_association, &block)
148
152
  end
149
153
 
150
154
  if where_parameters != []
151
155
  result = result.where(*where_parameters)
152
156
  end
153
157
 
158
+ result = yield result if block_given?
154
159
  [result]
155
160
  end
156
161
 
157
- def where_exists_for_habtm_query(association, where_parameters, next_association = {})
162
+ def where_exists_for_habtm_query(association, where_parameters, next_association = {}, &block)
158
163
  association_scope = association.scope
159
164
 
160
165
  associated_model = association.klass
@@ -178,7 +183,7 @@ module WhereExists
178
183
  where("#{join_ids} = #{self_ids}")
179
184
 
180
185
  if next_association[:association]
181
- return loop_nested_association(result, next_association)
186
+ return loop_nested_association(result, next_association, &block)
182
187
  end
183
188
 
184
189
  if where_parameters != []
@@ -189,15 +194,18 @@ module WhereExists
189
194
  result = result.instance_exec(&association_scope)
190
195
  end
191
196
 
197
+ result = yield result if block_given?
198
+
192
199
  [result]
193
200
  end
194
201
 
195
- def loop_nested_association(query, next_association = {}, nested = false)
202
+ def loop_nested_association(query, next_association = {}, nested = false, &block)
196
203
  str = query.klass.build_exists_string(
197
204
  next_association[:association].name,
198
205
  *[
199
206
  *next_association[:params]
200
207
  ],
208
+ &block
201
209
  )
202
210
 
203
211
  if next_association[:next_association] && next_association[:next_association][:association]
@@ -206,7 +214,8 @@ module WhereExists
206
214
  "(#{subq} AND (#{loop_nested_association(
207
215
  next_association[:association],
208
216
  next_association[:next_association],
209
- true
217
+ true,
218
+ &block
210
219
  )}))"
211
220
  end
212
221
  end
@@ -61,6 +61,18 @@ class BelongsToPolymorphicTest < Minitest::Test
61
61
  assert_equal orphaned_child.id, result.first.id
62
62
  end
63
63
 
64
+ def test_no_entities_or_empty_child_relation
65
+ result = BelongsToPolymorphicChild.where_not_exists(:polymorphic_entity)
66
+ assert_equal 0, result.length
67
+
68
+ _first_child = BelongsToPolymorphicChild.create!
69
+ result = BelongsToPolymorphicChild.where_not_exists(:polymorphic_entity)
70
+ assert_equal 1, result.length
71
+
72
+ result = BelongsToPolymorphicChild.where_exists(:polymorphic_entity)
73
+ assert_equal 0, result.length
74
+ end
75
+
64
76
  def test_table_name_based_lookup
65
77
  first_entity = FirstPolymorphicEntity.create!
66
78
  second_entity = SecondPolymorphicEntity.create! id: first_entity.id + 1
@@ -52,8 +52,6 @@ class Blob < ActiveRecord::Base
52
52
  end
53
53
  end
54
54
 
55
-
56
-
57
55
  class Project < ActiveRecord::Base
58
56
  has_many :tasks
59
57
  has_many :invoices, :through => :tasks
@@ -99,8 +97,6 @@ class HasManyThroughTest < Minitest::Test
99
97
  end
100
98
 
101
99
  def test_one_level_through
102
- ActiveRecord::Base.descendants.each(&:delete_all)
103
-
104
100
  project = Project.create!
105
101
  irrelevant_project = Project.create!
106
102
 
@@ -121,8 +117,6 @@ class HasManyThroughTest < Minitest::Test
121
117
  end
122
118
 
123
119
  def test_deep_through
124
- ActiveRecord::Base.descendants.each(&:delete_all)
125
-
126
120
  project = Project.create! name: 'relevant'
127
121
  irrelevant_project = Project.create! name: 'irrelevant'
128
122
 
@@ -188,6 +182,14 @@ class HasManyThroughTest < Minitest::Test
188
182
  result = Project.where_not_exists(:blobs)
189
183
 
190
184
  assert_equal 0, result.length
185
+ end
191
186
 
187
+ def test_with_yield
188
+ project = Project.create! name: 'example_project'
189
+ task = Task.create!(project: project)
190
+ line_item = LineItem.create!(name: 'example_line_item', task: task)
191
+ result = Project.where_exists(:project_line_items) { |scope| scope.where(name: 'example_line_item') }
192
+
193
+ assert_equal 1, result.length
192
194
  end
193
195
  end
metadata CHANGED
@@ -1,49 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: where_exists
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene Zolotarev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-19 00:00:00.000000000 Z
11
+ date: 2022-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.1'
22
+ version: '7.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '4.2'
29
+ version: '5.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.1'
32
+ version: '7.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: sqlite3
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '1.3'
39
+ version: '1.4'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.3'
46
+ version: '1.4'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: minitest
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,20 @@ dependencies:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
88
  version: '6.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: appraisal
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
89
103
  description: Rails way to harness the power of SQL "EXISTS" statement
90
104
  email:
91
105
  - eugzol@gmail.com
@@ -94,12 +108,12 @@ extensions: []
94
108
  extra_rdoc_files: []
95
109
  files:
96
110
  - MIT-LICENSE
111
+ - README.markdown
97
112
  - Rakefile
98
113
  - lib/where_exists.rb
99
114
  - lib/where_exists/version.rb
100
115
  - test/belongs_to_polymorphic_test.rb
101
116
  - test/belongs_to_test.rb
102
- - test/db/test.db
103
117
  - test/documentation_test.rb
104
118
  - test/has_and_belongs_to_many.rb
105
119
  - test/has_many_polymorphic_test.rb
@@ -125,17 +139,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
139
  - !ruby/object:Gem::Version
126
140
  version: '0'
127
141
  requirements: []
128
- rubygems_version: 3.0.2
142
+ rubygems_version: 3.1.6
129
143
  signing_key:
130
144
  specification_version: 4
131
145
  summary: "#where_exists extension of ActiveRecord"
132
146
  test_files:
147
+ - test/documentation_test.rb
133
148
  - test/belongs_to_polymorphic_test.rb
134
149
  - test/belongs_to_test.rb
135
- - test/db/test.db
136
- - test/documentation_test.rb
137
- - test/has_and_belongs_to_many.rb
138
150
  - test/has_many_polymorphic_test.rb
139
- - test/has_many_test.rb
140
151
  - test/has_many_through_test.rb
141
152
  - test/test_helper.rb
153
+ - test/has_and_belongs_to_many.rb
154
+ - test/has_many_test.rb
data/test/db/test.db DELETED
Binary file