├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── Appraisals
├── Gemfile
├── MIT-LICENSE
├── README.markdown
├── Rakefile
├── exists.png
├── gemfiles
├── 7.0.gemfile
├── 7.1.gemfile
├── 7.2.gemfile
└── 8.0.gemfile
├── lib
├── where_exists.rb
└── where_exists
│ └── version.rb
├── test
├── belongs_to_polymorphic_test.rb
├── belongs_to_test.rb
├── documentation_test.rb
├── has_and_belongs_to_many.rb
├── has_many_polymorphic_test.rb
├── has_many_test.rb
├── has_many_through_test.rb
└── test_helper.rb
└── where_exists.gemspec
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | ruby: ['2.7', '3.2', '3.3']
12 | rails: ['7.0', '7.1', '7.2', '8.0']
13 | exclude:
14 | - ruby: '2.7'
15 | rails: '8.0'
16 | - ruby: '2.7'
17 | rails: '7.2'
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Set up Ruby ${{ matrix.ruby }}
22 | uses: ruby/setup-ruby@v1
23 | env:
24 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
25 | with:
26 | ruby-version: ${{ matrix.ruby }}
27 | bundler-cache: true
28 |
29 | - name: Install deps
30 | run: |
31 | sudo apt update -y
32 | sudo apt install -y libsqlite3-dev
33 |
34 | - name: Compile & test
35 | env:
36 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
37 | run: |
38 | bundle exec rake
39 |
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | test/db/**
5 | *.gem
6 | Gemfile.lock
7 | gemfiles/*.lock
8 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise '7.0' do
2 | gem 'rails', '~> 7.0'
3 | end
4 |
5 | appraise '7.1' do
6 | gem 'rails', '~> 7.1'
7 | end
8 |
9 | appraise '7.2' do
10 | gem 'rails', '~> 7.2'
11 | end
12 |
13 | appraise '8.0' do
14 | gem 'rails', '~> 8.0'
15 | end
16 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Declare your gem's dependencies in where_exists.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # Declare any dependencies that are still in development here instead of in
9 | # your gemspec. These might include edge Rails or gems from your path or
10 | # Git. Remember to move these dependencies to your gemspec before releasing
11 | # your gem to rubygems.org.
12 |
13 | # To use debugger
14 | # gem 'debugger'
15 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Eugene Zolotarev
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # Where Exists
2 | **Rails way to harness the power of SQL EXISTS condition**
3 | [](http://badge.fury.io/rb/where_exists)
4 |
5 | ## Description
6 |
7 |
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 | 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).
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 | Notice that the second argument is `where` parameters for Group model
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 | It is possible to add as much attributes to the criteria as it is necessary, just as with regular `where(...)`
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 | Adding parameters (the second argument) to `where_not_exists` method is feasible as well, if you have such requirements.
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 | 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.
124 |
125 |
126 |
127 | ## Additional capabilities
128 |
129 | **Q**: Does it support both `has_many` and `belongs_to` association type?
130 | **A**: Yes.
131 |
132 |
133 | **Q**: Does it support polymorphic associations?
134 | **A**: Yes, both ways.
135 |
136 |
137 | **Q**: Does it support multi-level (recursive) `:through` associations?
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)`?
142 | **A**: Yes.
143 |
144 |
145 | **Q**: Does it take into account default association condition, e.g. `has_many :drafts, -> { where published: nil }`?
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
175 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler/setup'
3 | rescue LoadError
4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5 | end
6 |
7 | require 'rdoc/task'
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = 'rdoc'
11 | rdoc.title = 'WhereExists'
12 | rdoc.options << '--line-numbers'
13 | rdoc.rdoc_files.include('README.rdoc')
14 | rdoc.rdoc_files.include('lib/**/*.rb')
15 | end
16 |
17 |
18 |
19 |
20 | Bundler::GemHelper.install_tasks
21 |
22 | require 'rake/testtask'
23 |
24 | Rake::TestTask.new(:test) do |t|
25 | t.libs << 'lib'
26 | t.libs << 'test'
27 | t.pattern = 'test/**/*_test.rb'
28 | t.verbose = true
29 | end
30 |
31 |
32 | task default: :test
33 |
--------------------------------------------------------------------------------
/exists.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EugZol/where_exists/c9cc0d5eb399526b0c85dfe250ac8750e48770e2/exists.png
--------------------------------------------------------------------------------
/gemfiles/7.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 7.0"
6 |
7 | gemspec path: "../"
8 |
--------------------------------------------------------------------------------
/gemfiles/7.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 7.1"
6 |
7 | gemspec path: "../"
8 |
--------------------------------------------------------------------------------
/gemfiles/7.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 7.2"
6 |
7 | gemspec path: "../"
8 |
--------------------------------------------------------------------------------
/gemfiles/8.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 8.0"
6 |
7 | gemspec path: "../"
8 |
--------------------------------------------------------------------------------
/lib/where_exists.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | module WhereExists
4 | def where_exists(association_name, *where_parameters, &block)
5 | where_exists_or_not_exists(true, association_name, where_parameters, &block)
6 | end
7 |
8 | def where_not_exists(association_name, *where_parameters, &block)
9 | where_exists_or_not_exists(false, association_name, where_parameters, &block)
10 | end
11 |
12 | protected
13 |
14 | def where_exists_or_not_exists(does_exist, association_name, where_parameters, &block)
15 | queries_sql = build_exists_string(association_name, *where_parameters, &block)
16 |
17 | if does_exist
18 | not_string = ""
19 | else
20 | not_string = "NOT "
21 | end
22 |
23 | if queries_sql.empty?
24 | does_exist ? self.none : self.all
25 | else
26 | self.where("#{not_string}(#{queries_sql})")
27 | end
28 | end
29 |
30 | def build_exists_string(association_name, *where_parameters, &block)
31 | association = self.reflect_on_association(association_name)
32 |
33 | unless association
34 | raise ArgumentError.new("where_exists: association - #{association_name} - #{association_name.inspect} not found on #{self.name}")
35 | end
36 |
37 | case association.macro
38 | when :belongs_to
39 | queries = where_exists_for_belongs_to_query(association, where_parameters, &block)
40 | when :has_many, :has_one
41 | queries = where_exists_for_has_many_query(association, where_parameters, &block)
42 | when :has_and_belongs_to_many
43 | queries = where_exists_for_habtm_query(association, where_parameters, &block)
44 | else
45 | inspection = nil
46 | begin
47 | inspection = association.macros.inspect
48 | rescue
49 | inspection = association.macro
50 | end
51 | raise ArgumentError.new("where_exists: not supported association - #{inspection}")
52 | end
53 |
54 | queries_sql =
55 | queries.map do |query|
56 | "EXISTS (" + query.to_sql + ")"
57 | end
58 | queries_sql.join(" OR ")
59 | end
60 |
61 | def where_exists_for_belongs_to_query(association, where_parameters, &block)
62 | polymorphic = association.options[:polymorphic].present?
63 |
64 | association_scope = association.scope
65 |
66 | if polymorphic
67 | associated_models = self.select("DISTINCT #{connection.quote_column_name(association.foreign_type)}").
68 | where("#{connection.quote_column_name(association.foreign_type)} IS NOT NULL").pluck(association.foreign_type).
69 | uniq.map(&:classify).map(&:constantize)
70 | else
71 | associated_models = [association.klass]
72 | end
73 |
74 | queries = []
75 |
76 | self_ids = quote_table_and_column_name(self.table_name, association.foreign_key)
77 | self_type = quote_table_and_column_name(self.table_name, association.foreign_type)
78 |
79 | associated_models.each do |associated_model|
80 | primary_key = association.options[:primary_key] || associated_model.primary_key
81 | other_ids = quote_table_and_column_name(associated_model.table_name, primary_key)
82 | query = associated_model.select("1").where("#{self_ids} = #{other_ids}")
83 | if where_parameters != []
84 | query = query.where(*where_parameters)
85 | end
86 | if association_scope
87 | query = query.instance_exec(&association_scope)
88 | end
89 | if polymorphic
90 | other_types = [associated_model.name, associated_model.table_name]
91 | other_types << associated_model.polymorphic_name if associated_model.respond_to?(:polymorphic_name)
92 |
93 | query = query.where("#{self_type} IN (?)", other_types.uniq)
94 | end
95 | query = yield query if block_given?
96 | queries.push query
97 | end
98 |
99 | queries
100 | end
101 |
102 | def where_exists_for_has_many_query(association, where_parameters, next_association = {}, &block)
103 | if association.through_reflection
104 | raise ArgumentError.new(association) unless association.source_reflection
105 | next_association = {
106 | association: association.source_reflection,
107 | params: where_parameters,
108 | next_association: next_association
109 | }
110 | association = association.through_reflection
111 |
112 | case association.macro
113 | when :has_many, :has_one
114 | return where_exists_for_has_many_query(association, {}, next_association, &block)
115 | when :has_and_belongs_to_many
116 | return where_exists_for_habtm_query(association, {}, next_association, &block)
117 | else
118 | inspection = nil
119 | begin
120 | inspection = association.macros.inspect
121 | rescue
122 | inspection = association.macro
123 | end
124 | raise ArgumentError.new("where_exists: not supported association - #{inspection}")
125 | end
126 | end
127 |
128 | association_scope = next_association[:scope] || association.scope
129 |
130 | associated_model = association.klass
131 | primary_key = association.options[:primary_key] || self.primary_key
132 |
133 | self_ids = quote_table_and_column_name(self.table_name, primary_key)
134 | associated_ids = quote_table_and_column_name(associated_model.table_name, association.foreign_key)
135 |
136 | result = associated_model.select("1").where("#{associated_ids} = #{self_ids}")
137 |
138 | if association.options[:as]
139 | other_types = quote_table_and_column_name(associated_model.table_name, association.type)
140 | class_values = [self.name, self.table_name]
141 | class_values << self.polymorphic_name if associated_model.respond_to?(:polymorphic_name)
142 |
143 | result = result.where("#{other_types} IN (?)", class_values.uniq)
144 | end
145 |
146 | if association_scope
147 | result = result.instance_exec(&association_scope)
148 | end
149 |
150 | if next_association[:association]
151 | return loop_nested_association(result, next_association, &block)
152 | end
153 |
154 | if where_parameters != []
155 | result = result.where(*where_parameters)
156 | end
157 |
158 | result = yield result if block_given?
159 | [result]
160 | end
161 |
162 | def where_exists_for_habtm_query(association, where_parameters, next_association = {}, &block)
163 | association_scope = association.scope
164 |
165 | associated_model = association.klass
166 |
167 | primary_key = association.options[:primary_key] || self.primary_key
168 |
169 | self_ids = quote_table_and_column_name(self.table_name, primary_key)
170 | join_ids = quote_table_and_column_name(association.join_table, association.foreign_key)
171 | associated_join_ids = quote_table_and_column_name(association.join_table, association.association_foreign_key)
172 | associated_ids = quote_table_and_column_name(associated_model.table_name, associated_model.primary_key)
173 |
174 | result =
175 | associated_model.
176 | select("1").
177 | joins(
178 | <<-SQL
179 | INNER JOIN #{connection.quote_table_name(association.join_table)}
180 | ON #{associated_ids} = #{associated_join_ids}
181 | SQL
182 | ).
183 | where("#{join_ids} = #{self_ids}")
184 |
185 | if next_association[:association]
186 | return loop_nested_association(result, next_association, &block)
187 | end
188 |
189 | if where_parameters != []
190 | result = result.where(*where_parameters)
191 | end
192 |
193 | if association_scope
194 | result = result.instance_exec(&association_scope)
195 | end
196 |
197 | result = yield result if block_given?
198 |
199 | [result]
200 | end
201 |
202 | def loop_nested_association(query, next_association = {}, nested = false, &block)
203 | str = query.klass.build_exists_string(
204 | next_association[:association].name,
205 | *[
206 | *next_association[:params]
207 | ],
208 | &block
209 | )
210 |
211 | if next_association[:next_association] && next_association[:next_association][:association]
212 | subq = str.match(/\([^\(\)]+\)/mi)[0]
213 | str.sub!(subq) do
214 | "(#{subq} AND (#{loop_nested_association(
215 | next_association[:association],
216 | next_association[:next_association],
217 | true,
218 | &block
219 | )}))"
220 | end
221 | end
222 |
223 | nested ? str : [query.where(str)]
224 | end
225 |
226 | def quote_table_and_column_name(table_name, column_name)
227 | connection.quote_table_name(table_name) + '.' + connection.quote_column_name(column_name)
228 | end
229 | end
230 |
231 | class ActiveRecord::Base
232 | extend WhereExists
233 | end
234 |
--------------------------------------------------------------------------------
/lib/where_exists/version.rb:
--------------------------------------------------------------------------------
1 | module WhereExists
2 | VERSION = "3.0.0"
3 | end
4 |
--------------------------------------------------------------------------------
/test/belongs_to_polymorphic_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :first_polymorphic_entities, :force => true do |t|
4 | t.string :name
5 | end
6 |
7 | ActiveRecord::Migration.create_table :second_polymorphic_entities, :force => true do |t|
8 | t.string :name
9 | end
10 |
11 | ActiveRecord::Migration.create_table :belongs_to_polymorphic_children, :force => true do |t|
12 | t.integer :polymorphic_entity_id
13 | t.string :polymorphic_entity_type
14 | t.string :name
15 | end
16 |
17 | class BelongsToPolymorphicChild < ActiveRecord::Base
18 | belongs_to :polymorphic_entity, polymorphic: true
19 | end
20 |
21 | class FirstPolymorphicEntity < ActiveRecord::Base
22 | has_many :children, as: :polymorphic_entity, class_name: 'BelongsToPolymorphicChild'
23 | end
24 |
25 | class SecondPolymorphicEntity < ActiveRecord::Base
26 | has_many :children, as: :polymorphic_entity, class_name: 'BelongsToPolymorphicChild'
27 | end
28 |
29 | class BelongsToPolymorphicTest < Minitest::Test
30 | def setup
31 | ActiveRecord::Base.descendants.each(&:delete_all)
32 | end
33 |
34 | def test_exists_only_one_kind
35 | first_entity = FirstPolymorphicEntity.create!
36 | second_entity = SecondPolymorphicEntity.create!
37 | second_entity.update_column(:id, first_entity.id + 1)
38 |
39 | first_child = BelongsToPolymorphicChild.create!(polymorphic_entity: first_entity)
40 | second_child = BelongsToPolymorphicChild.create!(polymorphic_entity: second_entity)
41 | _really_orphaned_child = BelongsToPolymorphicChild.create!(polymorphic_entity_type: 'FirstPolymorphicEntity', polymorphic_entity_id: second_entity.id)
42 | _another_really_orphaned_child = BelongsToPolymorphicChild.create!(polymorphic_entity_type: 'SecondPolymorphicEntity', polymorphic_entity_id: first_entity.id)
43 |
44 | result = BelongsToPolymorphicChild.where_exists(:polymorphic_entity)
45 |
46 | assert_equal 2, result.length
47 | assert_equal [first_child, second_child].map(&:id).sort, result.map(&:id).sort
48 | end
49 |
50 | def test_neither_exists
51 | first_entity = FirstPolymorphicEntity.create!
52 | second_entity = SecondPolymorphicEntity.create!
53 | second_entity.update_column(:id, first_entity.id + 1)
54 |
55 | _first_child = BelongsToPolymorphicChild.create!(polymorphic_entity: first_entity)
56 | orphaned_child = BelongsToPolymorphicChild.create!(polymorphic_entity_id: second_entity.id, polymorphic_entity_type: 'FirstPolymorphicEntity')
57 |
58 | result = BelongsToPolymorphicChild.where_not_exists(:polymorphic_entity)
59 |
60 | assert_equal 1, result.length
61 | assert_equal orphaned_child.id, result.first.id
62 | end
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 |
76 | def test_table_name_based_lookup
77 | first_entity = FirstPolymorphicEntity.create!
78 | second_entity = SecondPolymorphicEntity.create! id: first_entity.id + 1
79 |
80 | first_child = BelongsToPolymorphicChild.create!(polymorphic_entity_id: first_entity.id, polymorphic_entity_type: first_entity.class.table_name)
81 | second_child = BelongsToPolymorphicChild.create!(polymorphic_entity_id: second_entity.id, polymorphic_entity_type: second_entity.class.table_name)
82 | orphaned_child = BelongsToPolymorphicChild.create!(polymorphic_entity_id: second_entity.id, polymorphic_entity_type: first_entity.class.table_name)
83 |
84 | result = BelongsToPolymorphicChild.where_exists(:polymorphic_entity)
85 | assert_equal 2, result.length
86 | assert_equal [first_child, second_child].map(&:id).sort, result.map(&:id).sort
87 |
88 | result = BelongsToPolymorphicChild.where_not_exists(:polymorphic_entity)
89 | assert_equal 1, result.length
90 | assert_equal [orphaned_child].map(&:id).sort, result.map(&:id).sort
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/test/belongs_to_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :belongs_to_simple_entities, :force => true do |t|
4 | t.string :name
5 | t.integer :my_id
6 | end
7 |
8 | ActiveRecord::Migration.create_table :belongs_to_simple_entity_children, :force => true do |t|
9 | t.integer :parent_id
10 | t.string :name
11 | end
12 |
13 | class BelongsToSimpleEntity < ActiveRecord::Base
14 | has_many :simple_entity_children, primary_key: :my_id, foreign_key: :parent_id, class_name: 'BelongsToSimpleEntityChild'
15 | has_many :unnamed_children, -> { where name: nil }, primary_key: :my_id, foreign_key: :parent_id, class_name: 'BelongsToSimpleEntityChild'
16 | end
17 |
18 | class BelongsToSimpleEntityChild < ActiveRecord::Base
19 | belongs_to :simple_entity, foreign_key: :parent_id, primary_key: :my_id, class_name: 'BelongsToSimpleEntity'
20 | end
21 |
22 | class BelongsToTest < Minitest::Test
23 | def setup
24 | ActiveRecord::Base.descendants.each(&:delete_all)
25 | end
26 |
27 | def test_nil_foreign_key
28 | _entity = BelongsToSimpleEntity.create!(my_id: 999)
29 |
30 | child = BelongsToSimpleEntityChild.create!(parent_id: 999)
31 | _orphaned_child = BelongsToSimpleEntityChild.create!(parent_id: nil)
32 |
33 | result = BelongsToSimpleEntityChild.where_exists(:simple_entity)
34 |
35 | assert_equal 1, result.length
36 | assert_equal result.first.id, child.id
37 | end
38 |
39 | def test_not_existing_foreign_object
40 | _entity = BelongsToSimpleEntity.create!(my_id: 999)
41 |
42 | child = BelongsToSimpleEntityChild.create!(parent_id: 999)
43 | _orphaned_child = BelongsToSimpleEntityChild.create!(parent_id: 500)
44 |
45 | result = BelongsToSimpleEntityChild.where_exists(:simple_entity)
46 |
47 | assert_equal 1, result.length
48 | assert_equal result.first.id, child.id
49 | end
50 |
51 | def test_with_parameters
52 | wrong_child = BelongsToSimpleEntityChild.create!(name: 'wrong')
53 | child = BelongsToSimpleEntityChild.create!(name: 'right')
54 |
55 | _blank_entity = BelongsToSimpleEntity.create!(my_id: 999)
56 | _wrong_entity = BelongsToSimpleEntity.create!(simple_entity_children: [wrong_child], my_id: 500)
57 | entity = BelongsToSimpleEntity.create!(name: 'this field is irrelevant', simple_entity_children: [child], my_id: 300)
58 |
59 | result = BelongsToSimpleEntity.where_exists(:simple_entity_children, name: 'right')
60 |
61 | assert_equal 1, result.length
62 | assert_equal result.first.id, entity.id
63 | end
64 |
65 | def test_with_condition
66 | child_1 = BelongsToSimpleEntityChild.create! name: nil
67 | child_2 = BelongsToSimpleEntityChild.create! name: 'Luke'
68 |
69 | entity_1 = BelongsToSimpleEntity.create!(simple_entity_children: [child_1], my_id: 999)
70 | entity_2 = BelongsToSimpleEntity.create!(simple_entity_children: [child_2], my_id: 500)
71 |
72 | result = BelongsToSimpleEntity.unscoped.where_exists(:unnamed_children)
73 | assert_equal 1, result.length
74 | assert_equal result.first.id, entity_1.id
75 |
76 | result = BelongsToSimpleEntity.unscoped.where_not_exists(:unnamed_children)
77 | assert_equal 1, result.length
78 | assert_equal result.first.id, entity_2.id
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/test/documentation_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :users, :force => true do |t|
4 | t.string :name
5 | end
6 |
7 | ActiveRecord::Migration.create_table :groups, :force => true do |t|
8 | t.string :name
9 | end
10 |
11 | ActiveRecord::Migration.create_table :connections, :force => true do |t|
12 | t.integer :user_id
13 | t.integer :group_id
14 | t.string :name
15 | end
16 |
17 | class User < ActiveRecord::Base
18 | has_many :connections
19 | has_many :groups, through: :connections
20 | end
21 |
22 | class Group < ActiveRecord::Base
23 | has_many :connections
24 | has_many :users, through: :connections
25 | end
26 |
27 | class Connection < ActiveRecord::Base
28 | belongs_to :user
29 | belongs_to :group
30 | end
31 |
32 | class DocumentationTest < Minitest::Test
33 | def setup
34 | ActiveRecord::Base.descendants.each(&:delete_all)
35 | end
36 |
37 | def test_readme
38 | group1 = Group.create!(name: 'first')
39 | group2 = Group.create!(name: 'second')
40 | _group3 = Group.create!(name: 'third')
41 |
42 | _group4 = Group.create!(name: 'fourth')
43 | group5 = Group.create!(name: 'fifth')
44 | group6 = Group.create!(name: 'sixth')
45 |
46 | user1 = User.create!
47 | Connection.create!(user: user1, group: group1)
48 |
49 | user2 = User.create!
50 | Connection.create!(user: user2, group: group2)
51 | Connection.create!(user: user2, group: group6)
52 |
53 | user3 = User.create!
54 | Connection.create!(user: user3, group: group5)
55 |
56 | user4 = User.create!
57 |
58 | result = User.where_exists(:groups, id: [1,2,3])
59 | assert_equal 2, result.length
60 | assert_equal [user1, user2].map(&:id).sort, result.map(&:id).sort
61 |
62 | result = User.where_exists(:groups, id: [1,2,3]).where_not_exists(:groups, name: %w(fourth fifth sixth))
63 | assert_equal 1, result.length
64 | assert_equal user1.id, result.first.id
65 |
66 | result = User.where_not_exists(:groups)
67 | assert_equal 1, result.length
68 | assert_equal user4.id, result.first.id
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/test/has_and_belongs_to_many.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :tasks, :force => true do |t|
4 | t.string :name
5 | end
6 |
7 | ActiveRecord::Migration.create_table :invoices_tasks, :force => true do |t|
8 | t.integer :invoice_id
9 | t.integer :task_id
10 | end
11 |
12 | ActiveRecord::Migration.create_table :invoices, :force => true do |t|
13 | t.string :name
14 | end
15 |
16 | class Task < ActiveRecord::Base
17 | has_and_belongs_to_many :connected_invoices, class_name: 'Invoice'
18 | end
19 |
20 | class Invoice < ActiveRecord::Base
21 | has_and_belongs_to_many :tasks
22 | has_and_belongs_to_many :unnamed_tasks, -> { where(name: nil) }
23 | end
24 |
25 | # Invoices -> LineItems <- Tasks <- Project
26 |
27 | class HasAndBelongsToManyTest < Minitest::Test
28 | def setup
29 | ActiveRecord::Base.descendants.each(&:delete_all)
30 | end
31 |
32 | def test_with_standard_naming
33 | task = Task.create!(name: 'task')
34 | irrelevant_task = Task.create!(name: 'task_2')
35 | invoice = Invoice.create!(name: 'invoice')
36 | invoice_no_join = Invoice.create!(name: 'invoice_2')
37 |
38 | invoice.tasks << task
39 |
40 |
41 | result = Invoice.where_exists(:tasks, name: 'task')
42 |
43 | assert_equal 1, result.length
44 | assert_equal invoice.id, result.first.id
45 |
46 | result = Invoice.where_exists(:tasks, name: 'task_2')
47 |
48 | assert_equal 0, result.length
49 |
50 | result = Invoice.where_not_exists(:tasks)
51 | assert_equal 1, result.length
52 | assert_equal invoice_no_join.id, result.first.id
53 | end
54 |
55 | def test_with_custom_naming
56 | task = Task.create!(name: 'task')
57 | task_no_join = Task.create!(name: 'invoice')
58 | invoice = Invoice.create!(name: 'invoice')
59 | irrelevant_invoice = Invoice.create!(name: 'invoice_2')
60 |
61 | task.connected_invoices << invoice
62 |
63 | result = Task.where_exists(:connected_invoices, name: 'invoice')
64 |
65 | assert_equal 1, result.length
66 | assert_equal task.id, result.first.id
67 |
68 | result = Task.where_exists(:connected_invoices, name: 'invoice_2')
69 |
70 | assert_equal 0, result.length
71 |
72 | result = Task.where_not_exists(:connected_invoices)
73 | assert_equal 1, result.length
74 | assert_equal task_no_join.id, result.first.id
75 |
76 | result = Task.where_not_exists(:connected_invoices, name: 'invoice_2')
77 | assert_equal 2, result.length
78 | end
79 |
80 | def test_with_condition
81 | task_1 = Task.create! name: nil
82 | task_2 = Task.create! name: 'task 2'
83 |
84 | invoice_1 = Invoice.create!(tasks: [task_1])
85 | invoice_2 = Invoice.create!(tasks: [task_1, task_2])
86 | invoice_3 = Invoice.create!(tasks: [task_2)
87 |
88 | result_ids = Invoice.where_exists(:unnamed_tasks).pluck(:id)
89 |
90 | assert_equal [invoice_1.id, invoice_2.id].sort, result_ids.sort
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/test/has_many_polymorphic_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :relevant_polymorphic_entities, :force => true do |t|
4 | t.string :name
5 | end
6 |
7 | ActiveRecord::Migration.create_table :irrelevant_polymorphic_entities, :force => true do |t|
8 | t.string :name
9 | end
10 |
11 | ActiveRecord::Migration.create_table :has_many_polymorphic_children, :force => true do |t|
12 | t.integer :polymorphic_thing_id
13 | t.string :polymorphic_thing_type
14 | t.string :name
15 | end
16 |
17 | class HasManyPolymorphicChild < ActiveRecord::Base
18 | belongs_to :polymorphic_thing, polymorphic: true
19 | end
20 |
21 | class RelevantPolymorphicEntity < ActiveRecord::Base
22 | has_many :children, as: :polymorphic_thing, class_name: 'HasManyPolymorphicChild'
23 | end
24 |
25 | class IrrelevantPolymorphicEntity < ActiveRecord::Base
26 | has_many :children, as: :polymorphic_thing, class_name: 'HasManyPolymorphicChild'
27 | end
28 |
29 | class HasManyPolymorphicTest < Minitest::Test
30 | def setup
31 | ActiveRecord::Base.descendants.each(&:delete_all)
32 | end
33 |
34 | def test_polymorphic
35 | child = HasManyPolymorphicChild.create!
36 |
37 | irrelevant_entity = IrrelevantPolymorphicEntity.create!(children: [child])
38 | relevant_entity = RelevantPolymorphicEntity.create!(id: irrelevant_entity.id)
39 |
40 | assert_equal 0, RelevantPolymorphicEntity.where_exists(:children).length
41 | assert_equal 1, IrrelevantPolymorphicEntity.where_exists(:children).length
42 |
43 | child.update!(polymorphic_thing_type: RelevantPolymorphicEntity.table_name)
44 |
45 | result = RelevantPolymorphicEntity.where_exists(:children)
46 |
47 | assert_equal 0, IrrelevantPolymorphicEntity.where_exists(:children).length
48 | assert_equal 1, result.length
49 | assert_equal relevant_entity.id, result.first&.id
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/has_many_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :simple_entities, :force => true do |t|
4 | t.string :name
5 | t.integer :my_id
6 | end
7 |
8 | ActiveRecord::Migration.create_table :simple_entity_children, :force => true do |t|
9 | t.integer :parent_id
10 | t.datetime :my_date
11 | t.string :name
12 | end
13 |
14 | class SimpleEntity < ActiveRecord::Base
15 | has_many :simple_entity_children, primary_key: :my_id, foreign_key: :parent_id
16 | has_many :unnamed_children, -> { where name: nil }, primary_key: :my_id, foreign_key: :parent_id, class_name: 'SimpleEntityChild'
17 | end
18 |
19 | class SimpleEntityChild < ActiveRecord::Base
20 | belongs_to :simple_entity, foreign_key: :parent_id
21 | end
22 |
23 | class HasManyTest < Minitest::Test
24 | def setup
25 | ActiveRecord::Base.descendants.each(&:delete_all)
26 | end
27 |
28 | def test_without_parameters
29 | child = SimpleEntityChild.create!
30 |
31 | _blank_entity = SimpleEntity.create!(my_id: 999)
32 | filled_entity = SimpleEntity.create!(simple_entity_children: [child], my_id: 500)
33 |
34 | result = SimpleEntity.where_exists(:simple_entity_children)
35 |
36 | assert_equal 1, result.length
37 | assert_equal result.first.id, filled_entity.id
38 | end
39 |
40 | def test_with_parameters
41 | wrong_child = SimpleEntityChild.create!(name: 'wrong')
42 | child = SimpleEntityChild.create!(name: 'right')
43 |
44 | _blank_entity = SimpleEntity.create!(my_id: 999)
45 | _wrong_entity = SimpleEntity.create!(simple_entity_children: [wrong_child], my_id: 500)
46 | entity = SimpleEntity.create!(name: 'this field is irrelevant', simple_entity_children: [child], my_id: 300)
47 |
48 | result = SimpleEntity.where_exists(:simple_entity_children, name: 'right')
49 |
50 | assert_equal 1, result.length
51 | assert_equal result.first.id, entity.id
52 | end
53 |
54 | def test_with_scope
55 | child = SimpleEntityChild.create!
56 | entity = SimpleEntity.create!(simple_entity_children: [child], my_id: 999)
57 |
58 | result = SimpleEntity.unscoped.where_exists(:simple_entity_children)
59 |
60 | assert_equal 1, result.length
61 | assert_equal result.first.id, entity.id
62 | end
63 |
64 | def test_with_condition
65 | child_1 = SimpleEntityChild.create! name: nil
66 | child_2 = SimpleEntityChild.create! name: 'Luke'
67 |
68 | entity_1 = SimpleEntity.create!(simple_entity_children: [child_1], my_id: 999)
69 | entity_2 = SimpleEntity.create!(simple_entity_children: [child_2], my_id: 500)
70 |
71 | result = SimpleEntity.unscoped.where_exists(:unnamed_children)
72 | assert_equal 1, result.length
73 | assert_equal result.first.id, entity_1.id
74 |
75 | result = SimpleEntity.unscoped.where_not_exists(:unnamed_children)
76 | assert_equal 1, result.length
77 | assert_equal result.first.id, entity_2.id
78 | end
79 |
80 | def test_not_exists
81 | child = SimpleEntityChild.create!
82 |
83 | blank_entity = SimpleEntity.create!(my_id: 999)
84 | _filled_entity = SimpleEntity.create!(simple_entity_children: [child], my_id: 500)
85 |
86 | result = SimpleEntity.where_not_exists(:simple_entity_children)
87 |
88 | assert_equal 1, result.length
89 | assert_equal result.first.id, blank_entity.id
90 | end
91 |
92 | def test_dynamic_scopes
93 | child_past = SimpleEntityChild.create! my_date: Time.now - 1.minute
94 | child_future = SimpleEntityChild.create! my_date: Time.now + 1.minute
95 |
96 | _blank_entity = SimpleEntity.create!(simple_entity_children: [child_future], my_id: 999)
97 | filled_entity = SimpleEntity.create!(simple_entity_children: [child_past], my_id: 500)
98 |
99 | result = SimpleEntity.where_exists(:simple_entity_children) {|scope|
100 | scope.where('my_date < ?', Time.now)
101 | }
102 |
103 | assert_equal 1, result.length
104 | assert_equal result.first.id, filled_entity.id
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/test/has_many_through_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | ActiveRecord::Migration.create_table :projects, force: true do |t|
4 | t.string :name
5 | end
6 |
7 | ActiveRecord::Migration.create_table :tasks, force: true do |t|
8 | t.string :name
9 | t.integer :project_id
10 | end
11 |
12 | ActiveRecord::Migration.create_table :line_items, force: true do |t|
13 | t.string :name
14 | t.integer :invoice_id
15 | t.integer :task_id
16 | end
17 |
18 | ActiveRecord::Migration.create_table :work_details, force: true do |t|
19 | t.string :name
20 | t.integer :line_item_id
21 | end
22 |
23 | ActiveRecord::Migration.create_table :invoices, force: true do |t|
24 | t.string :name
25 | end
26 |
27 | ActiveRecord::Migration.create_table :blobs, force: true do |t|
28 | end
29 |
30 | ActiveRecord::Migration.create_table :attachments, force: true do |t|
31 | t.string :name, null: false
32 | t.references :record, null: false, polymorphic: true, index: false
33 | t.references :blob, null: false
34 |
35 | t.datetime :created_at, null: false
36 |
37 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_attachments_uniqueness", unique: true
38 | end
39 |
40 | class Attachment < ActiveRecord::Base
41 | belongs_to :record, polymorphic: true, touch: true
42 | belongs_to :blob
43 | end
44 |
45 | class Blob < ActiveRecord::Base
46 | has_many :attachments
47 |
48 | scope :unattached, -> { left_joins(:attachments).where(Attachment.table_name => { blob_id: nil }) }
49 |
50 | before_destroy(prepend: true) do
51 | raise ActiveRecord::InvalidForeignKey if attachments.exists?
52 | end
53 | end
54 |
55 | class Project < ActiveRecord::Base
56 | has_many :tasks
57 | has_many :invoices, :through => :tasks
58 | has_many :project_line_items, :through => :tasks, :source => :line_items
59 | has_many :work_details, :through => :project_line_items
60 |
61 | has_many :attachments, as: :record
62 | has_many :blobs, through: :attachments, source: :blob
63 | has_many :relevant_attachments, -> { where(name: "relevant") }, as: :record, class_name: "Attachment", inverse_of: :record, dependent: false
64 | has_many :relevant_blobs, through: :relevant_attachments, class_name: "Blob", source: :blob
65 | has_many :irrelevant_attachments, -> { where(name: "irrelevant") }, as: :record, class_name: "Attachment", inverse_of: :record, dependent: false
66 | has_many :irrelevant_blobs, through: :irrelevant_attachments, class_name: "Blob", source: :blob
67 | end
68 |
69 | class Task < ActiveRecord::Base
70 | belongs_to :project
71 |
72 | has_many :invoices, :through => :line_items
73 | has_many :line_items
74 | has_many :scoped_line_items, -> { where(name: 'relevant') }, class_name: 'LineItem'
75 | end
76 |
77 | class LineItem < ActiveRecord::Base
78 | belongs_to :invoice
79 | belongs_to :task
80 | has_many :work_details
81 | end
82 |
83 | class WorkDetail < ActiveRecord::Base
84 | belongs_to :line_item
85 | end
86 |
87 | class Invoice < ActiveRecord::Base
88 | has_many :tasks, :through => :line_item
89 | has_many :line_items
90 | end
91 |
92 | # Invoices -> LineItems <- Tasks <- Project
93 |
94 | class HasManyThroughTest < Minitest::Test
95 | def setup
96 | ActiveRecord::Base.descendants.each(&:delete_all)
97 | end
98 |
99 | def test_one_level_through
100 | project = Project.create!
101 | irrelevant_project = Project.create!
102 |
103 | task = Task.create!(project: project)
104 | irrelevant_task = Task.create!(project: irrelevant_project)
105 |
106 | _line_item = LineItem.create!(name: 'relevant', task: task)
107 | _irrelevant_line_item = LineItem.create!(name: 'irrelevant', task: irrelevant_task)
108 |
109 | result = Project.where_exists(:project_line_items, name: 'relevant')
110 |
111 | assert_equal 1, result.length
112 | assert_equal project.id, result.first.id
113 |
114 | result = Project.where_not_exists(:project_line_items, name: 'relevant')
115 | assert_equal 1, result.length
116 | assert_equal irrelevant_project.id, result.first.id
117 | end
118 |
119 | def test_deep_through
120 | project = Project.create! name: 'relevant'
121 | irrelevant_project = Project.create! name: 'irrelevant'
122 |
123 | task = Task.create!(project: project)
124 | irrelevant_task = Task.create!(project: irrelevant_project)
125 |
126 | invoice = Invoice.create!(name: 'relevant')
127 | irrelevant_invoice = Invoice.create!(name: 'irrelevant')
128 |
129 | line_item = LineItem.create!(name: 'relevant', task: task, invoice: invoice)
130 | irrelevant_line_item = LineItem.create!(name: 'relevant', task: irrelevant_task, invoice: irrelevant_invoice)
131 |
132 | _work_detail = WorkDetail.create!(line_item: line_item, name: 'relevant')
133 | _irrelevant_work_detail = WorkDetail.create!(line_item: irrelevant_line_item, name: 'irrelevant')
134 |
135 | blob = Blob.create!()
136 | _relevant_attachment = Attachment.create!(name: 'relevant', blob: blob, record: project)
137 | _irrelevant_attachment = Attachment.create!(name: 'irrelevant', blob: blob, record: irrelevant_project)
138 |
139 | result = Project.where_exists(:invoices, name: 'relevant')
140 |
141 | assert_equal 1, result.length
142 | assert_equal project.id, result.first.id
143 |
144 | result = Project.where_not_exists(:invoices, name: 'relevant')
145 |
146 | assert_equal 1, result.length
147 | assert_equal irrelevant_project.id, result.first.id
148 |
149 | result = Project.where_not_exists(:invoices, "name = ?", 'relevant')
150 |
151 | assert_equal 1, result.length
152 | assert_equal irrelevant_project.id, result.first.id
153 |
154 | result = Project.where_exists(:work_details, name: 'relevant')
155 |
156 | assert_equal 1, result.length
157 | assert_equal project.id, result.first.id
158 |
159 | result = Project.where_not_exists(:work_details, name: 'relevant')
160 |
161 | assert_equal 1, result.length
162 | assert_equal irrelevant_project.id, result.first.id
163 |
164 | result = Task.where_exists(:scoped_line_items)
165 |
166 | assert_equal 2, result.length
167 |
168 | result = Project.where_exists(:relevant_blobs)
169 |
170 | assert_equal 1, result.length
171 | assert_equal project.id, result.first.id
172 |
173 | result = Project.where_not_exists(:relevant_blobs)
174 |
175 | assert_equal 1, result.length
176 | assert_equal irrelevant_project.id, result.first.id
177 |
178 | result = Project.where_exists(:blobs)
179 |
180 | assert_equal 2, result.length
181 |
182 | result = Project.where_not_exists(:blobs)
183 |
184 | assert_equal 0, result.length
185 | end
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
194 | end
195 | end
196 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/pride'
3 | require 'bundler/setup'
4 | Bundler.require(:default)
5 | require 'active_record'
6 | require File.dirname(__FILE__) + '/../lib/where_exists'
7 |
8 | # Rails < 7.1
9 | if ActiveRecord::Base.respond_to?(:default_timezone=)
10 | ActiveRecord::Base.default_timezone = :utc
11 | else
12 | ActiveRecord.default_timezone = :utc
13 | end
14 |
15 | ActiveRecord::Base.time_zone_aware_attributes = true
16 |
17 | ActiveRecord::Base.establish_connection(
18 | :adapter => 'sqlite3',
19 | :database => File.dirname(__FILE__) + '/db/test.db'
20 | )
21 |
--------------------------------------------------------------------------------
/where_exists.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require "where_exists/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "where_exists"
9 | s.version = WhereExists::VERSION
10 | s.authors = ["Eugene Zolotarev"]
11 | s.email = ["eugzol@gmail.com"]
12 | s.homepage = "http://github.com/eugzol/where_exists"
13 | s.summary = "#where_exists extension of ActiveRecord"
14 | s.description = 'Rails way to harness the power of SQL "EXISTS" statement'
15 | s.license = "MIT"
16 |
17 | s.files = Dir["lib/**/*", "MIT-LICENSE", "Rakefile", "README.markdown"]
18 | s.test_files = Dir["test/**/*"]
19 |
20 | s.add_dependency "activerecord", ">= 5.2", "< 8.1"
21 |
22 | s.add_development_dependency "sqlite3", ">= 1.4"
23 | s.add_development_dependency "minitest", "~> 5.10"
24 | s.add_development_dependency "rake", "~> 12.3"
25 | s.add_development_dependency "rdoc", "~> 6.0"
26 | s.add_development_dependency "appraisal"
27 | end
28 |
--------------------------------------------------------------------------------