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 +4 -4
- data/README.markdown +174 -0
- data/lib/where_exists/version.rb +1 -1
- data/lib/where_exists.rb +23 -14
- data/test/belongs_to_polymorphic_test.rb +12 -0
- data/test/has_many_through_test.rb +8 -6
- metadata +28 -15
- data/test/db/test.db +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9957b5787ab2fda2505560b01ffeca8f2e1d24bf0d0d38e09f4f25969a30ab6
|
4
|
+
data.tar.gz: 4718917be7477f54afb0e322a588adadb4d2d375c6b7938d922b89ef0629ea27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
[](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
|
data/lib/where_exists/version.rb
CHANGED
data/lib/where_exists.rb
CHANGED
@@ -20,7 +20,11 @@ module WhereExists
|
|
20
20
|
not_string = "NOT "
|
21
21
|
end
|
22
22
|
|
23
|
-
|
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:
|
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:
|
11
|
+
date: 2022-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.2'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
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: '
|
29
|
+
version: '5.2'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
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.
|
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.
|
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.
|
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
|