├── .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 | [![Gem Version](https://badge.fury.io/rb/where_exists.svg)](http://badge.fury.io/rb/where_exists) 4 | 5 | ## Description 6 | 7 | Exists 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 | --------------------------------------------------------------------------------