where_exists 1.2.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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