Skip to content

Commit

Permalink
Merge branch 'rails-7' into 'master'
Browse files Browse the repository at this point in the history
ADD rails-7 support

See merge request oelmekki/activerecord_any_of!44
  • Loading branch information
oelmekki committed Jun 24, 2023
2 parents caebba6 + ad0f93c commit 2fd8dec
Show file tree
Hide file tree
Showing 18 changed files with 301 additions and 478 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.bundle/
log/*.log
pkg/
Gemfile.lock
gemfiles/*.gemfile.lock
spec/debug.log
coverage/
22 changes: 22 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
image: "ruby:latest"

stages:
- test

before_script:
- gem install bundler
- bundle install

rspec:
stage: test
script:
- bundle exec rspec
artifacts:
when: on_failure
paths:
- coverage

rubocop:
stage: test
script:
- bundle exec rubocop
6 changes: 6 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require:
- rubocop-rspec
- rubocop-rake

AllCops:
NewCops: enable
39 changes: 11 additions & 28 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
source 'https://rubygems.org'

gemspec :path => File.expand_path('../', __FILE__)

gem 'combustion', :github => 'pat/combustion', :branch => 'master'
# frozen_string_literal: true

platforms :jruby do
gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0.beta2'
gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0.beta2'
gem 'jdbc-mysql'
gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0.beta2'
gem 'jruby-openssl'
end

platforms :ruby do
gem 'sqlite3'
gem 'mysql2', (MYSQL2_VERSION if defined? MYSQL2_VERSION)
gem 'pg'
end

platforms :rbx do
gem 'rubysl', '~> 2.0'
gem 'rubysl-test-unit'
end
source 'https://rubygems.org'

# Add Oracle Adapters
# gem 'ruby-oci8'
# gem 'activerecord-oracle_enhanced-adapter'
gemspec path: File.expand_path(__dir__)

# Debuggers
gem 'database_cleaner'
gem 'pry'
gem 'pry-nav'
gem 'rake'
gem 'rspec-rails'
gem 'rubocop'
gem 'rubocop-rake'
gem 'rubocop-rspec'
gem 'simplecov'
gem 'sqlite3'
99 changes: 30 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,67 @@ This gem provides `#any_of` and `#none_of` on ActiveRecord.

`#any_of` is inspired by [any_of from mongoid](http://two.mongoid.org/docs/querying/criteria.html#any_of).

Its main purpose is to both :
It was released before `#or` was implemented in ActiveRecord. Its main purpose was to both :

* remove the need to write a sql string when we want an `OR`
* allows to write dynamic `OR` queries, which would be a pain with a string

It can still be useful today given the various ways you can call it. While
ActiveRecord's `#or` only accepts relations, you can pass to `#any_of` and
`#none_of` the same kind of conditions you would pass to `#where`:


```ruby
User.where.any_of({ active: true }, ['offline = ?', required_status], 'posts_count > 0')
```

And you can still use relations, like AR's `#or`:

```ruby
inactive_users = User.not_activated
offline_users = User.offline

User.where.any_of(inactive_users, offline)
```

## Installation

In your Gemfile :

```
gem 'activerecord_any_of'
```

## Usage

### `#any_of`

It allows to compute an `OR` like query that leverages AR's `#where` syntax:


#### basics

```ruby
User.where.any_of(first_name: 'Joe', last_name: 'Joe')
# => SELECT * FROM users WHERE first_name = 'Joe' OR last_name = 'Joe'
```


#### grouped conditions

You can separate sets of hash condition by explicitly group them as hashes :

```ruby
User.where.any_of({first_name: 'John', last_name: 'Joe'}, {first_name: 'Simon', last_name: 'Joe'})
User.where.any_of({ first_name: 'John', last_name: 'Joe' }, { first_name: 'Simon', last_name: 'Joe' })
# => SELECT * FROM users WHERE ( first_name = 'John' AND last_name = 'Joe' ) OR ( first_name = 'Simon' AND last_name = 'Joe' )
```


#### it's plain #where syntax

Each `#any_of` set is the same kind you would have passed to #where :

```ruby
Client.where.any_of("orders_count = '2'", ["name = ?", 'Joe'], {email: '[email protected]'})
Client.where.any_of("orders_count = '2'", ["name = ?", 'Joe'], { email: '[email protected]' })
```


#### with relations

You can as well pass `#any_of` to other relations :
Expand All @@ -54,16 +75,14 @@ You can as well pass `#any_of` to other relations :
Client.where("orders_count = '2'").where.any_of({ email: '[email protected]' }, { email: '[email protected]' })
```


#### with associations

And with associations :

```ruby
User.find(1).posts.where.any_of({published: false}, "user_id IS NULL")
User.find(1).posts.where.any_of({ published: false }, 'user_id IS NULL')
```


#### dynamic OR queries

The best part is that `#any_of` accepts other relations as parameter, to help compute
Expand All @@ -81,64 +100,6 @@ inactive_users = User.where.any_of(banned_users, unconfirmed_users)

```ruby
banned_users = User.where(banned: true)
unconfirmed_users = User.where("confirmed_at IS NULL")
unconfirmed_users = User.where('confirmed_at IS NULL')
active_users = User.where.none_of(banned_users, unconfirmed_users)
```

## Rails-3

`activerecord_any_of` uses WhereChain, which has been introduced in rails-4. In
rails-3, simply call `#any_of` and `#none_of` directly, without using `#where` :

```ruby
manual_removal = User.where(id: params[:users][:destroy_ids])
User.any_of(manual_removal, "email like '%@example.com'", {banned: true})
@company.users.any_of(manual_removal, "email like '%@example.com'", {banned: true})
User.where(offline: false).any_of( manual_removal, "email like '%@example.com'", {banned: true})
```

## Installation

In your Gemfile :

```
gem 'activerecord_any_of'
```

Activerecord_any_of supports rails >= 3.2.13 and rails-4 (let me know if tests
pass for rails < 3.2.13, I may edit gem dependencies).


## Why not an `#or` method instead ?

```ruby
User.where( "email LIKE '%@example.com" ).where( active: true ).or( offline: true )
```

What does this query do ? `where (email LIKE '%@example.com' AND active = '1' )
OR offline = '1'` ? Or `where email LIKE '%@example.com' AND ( active = '1' OR
offline = '1' )` ? This can quickly get messy and counter intuitive.

The MongoId solution is quite elegant. Using `#any_of`, it is made clear which
conditions are grouped through `OR` and which are grouped through `AND` :

* `User.where( "email LIKE '%@example.com" ).where.any_of({ active: true }, { offline: true })`
* `fakes = User.where( "email LIKE '%@example.com'" ).where( active: true ); User.where.any_of( fakes, { offline: true })`


## Running test

Testing is done using TravisCI. You can use the wonderful [wwtd gem](https://github.com/grosser/wwtd) to run all tests locally. By default, the task to run is `bundle exec rake spec`, and will run against `sqlite3` in memory. You can change the database like so: `DB=postgresql bundle exec rake spec`. Please note that you may need to change the credentials for your database in the `database.yml` file. *Do not commit those changes.*

## Pull requests

This gem is extracted from a pull request made to activerecord core, and
still hope to be merged. So, any pull request here should respects usual
[Rails contributing rules](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-code)
when it makes sense (especially : coding conventions) to make integration
in source pull request easy.


## Licence

MIT-LICENSE.
6 changes: 4 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
require "bundler/gem_tasks"
task :default => :spec
# frozen_string_literal: true

require 'bundler/gem_tasks'
task default: :spec
27 changes: 13 additions & 14 deletions activerecord_any_of.gemspec
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
$:.push File.expand_path("../lib", __FILE__)
# frozen_string_literal: true

$LOAD_PATH.push File.expand_path('lib', __dir__)

# Maintain your gem's version:
require "activerecord_any_of/version"
require 'activerecord_any_of/version'

# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = "activerecord_any_of"
s.name = 'activerecord_any_of'
s.version = ActiverecordAnyOf::VERSION
s.authors = ["Olivier El Mekki"]
s.email = ["[email protected]"]
s.homepage = "https://github.com/oelmekki/activerecord_any_of"
s.authors = ['Olivier El Mekki']
s.email = ['[email protected]']
s.homepage = 'https://github.com/oelmekki/activerecord_any_of'
s.summary = "Mongoid's any_of like implementation for activerecord"
s.description = "Any_of allows to compute dynamic OR queries."
s.description = 'Any_of allows to compute dynamic OR queries.'
s.license = 'MIT'
s.required_ruby_version = '>= 2.7.0'

s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"]
s.test_files = Dir["spec/**/*"]
s.files = Dir['{app,config,db,lib}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'README.md']

s.add_dependency "activerecord", ">= 3.2.13", '< 6'
s.add_dependency 'activerecord', '>= 7', '< 8'

s.add_development_dependency 'rspec-rails', '~> 2.12'
s.add_development_dependency 'rake', '~> 10'
s.add_development_dependency 'combustion', '>= 0.5.1'
s.add_development_dependency 'database_cleaner'
s.metadata['rubygems_mfa_required'] = 'true'
end
5 changes: 0 additions & 5 deletions gemfiles/rails3.gemfile

This file was deleted.

5 changes: 0 additions & 5 deletions gemfiles/rails4.gemfile

This file was deleted.

5 changes: 0 additions & 5 deletions gemfiles/rails_edge.gemfile

This file was deleted.

44 changes: 8 additions & 36 deletions lib/activerecord_any_of.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# frozen_string_literal: true

require 'activerecord_any_of/alternative_builder'

module ActiverecordAnyOf
# Injected into WhereChain.
module Chained
# Returns a new relation, which includes results matching any of the conditions
# passed as parameters. You can think of it as a sql <tt>OR</tt> implementation :
Expand All @@ -12,7 +15,8 @@ module Chained
# You can separate sets of hash condition by explicitly group them as hashes :
#
# User.where.any_of({first_name: 'John', last_name: 'Joe'}, {first_name: 'Simon', last_name: 'Joe'})
# # => SELECT * FROM users WHERE ( first_name = 'John' AND last_name = 'Joe' ) OR ( first_name = 'Simon' AND last_name = 'Joe' )
# # => SELECT * FROM users WHERE ( first_name = 'John' AND last_name = 'Joe' ) OR
# ( first_name = 'Simon' AND last_name = 'Joe' )
#
#
# Each #any_of set is the same kind you would have passed to #where :
Expand All @@ -38,6 +42,7 @@ module Chained
# inactive_users = User.where.any_of(banned_users, unconfirmed_users)
def any_of(*queries)
raise ArgumentError, 'Called any_of() with no arguments.' if queries.none?

AlternativeBuilder.new(:positive, @scope, *queries).build
end

Expand All @@ -51,43 +56,10 @@ def any_of(*queries)
# active_users = User.where.none_of(banned_users, unconfirmed_users)
def none_of(*queries)
raise ArgumentError, 'Called none_of() with no arguments.' if queries.none?
AlternativeBuilder.new(:negative, @scope, *queries).build
end
end

module Deprecated
def any_of(*queries)
if ActiveRecord::VERSION::MAJOR >= 4
ActiveSupport::Deprecation.warn( "Calling #any_of directly is deprecated and will be removed in activerecord_any_of-1.2.\nPlease call it with #where : User.where.any_of(cond1, cond2)." )
end

raise ArgumentError, 'Called any_of() with no arguments.' if queries.none?
AlternativeBuilder.new(:positive, self, *queries).build
end

def none_of(*queries)
if ActiveRecord::VERSION::MAJOR >= 4
ActiveSupport::Deprecation.warn( "Calling #none_of directly is deprecated and will be removed in activerecord_any_of-1.2.\nPlease call it with #where : User.where.none_of(cond1, cond2)." )
end

raise ArgumentError, 'Called none_of() with no arguments.' if queries.none?
AlternativeBuilder.new(:negative, self, *queries).build
AlternativeBuilder.new(:negative, @scope, *queries).build
end
end
end

if ActiveRecord::VERSION::MAJOR >= 4
module ActiverecordAnyOfDelegation
delegate :any_of, to: :all
delegate :none_of, to: :all
end
else
module ActiverecordAnyOfDelegation
delegate :any_of, to: :scoped
delegate :none_of, to: :scoped
end
end

ActiveRecord::Relation.send(:include, ActiverecordAnyOf::Deprecated)
ActiveRecord::Relation::WhereChain.send(:include, ActiverecordAnyOf::Chained) if ActiveRecord::VERSION::MAJOR >= 4
ActiveRecord::Base.send(:extend, ActiverecordAnyOfDelegation)
ActiveRecord::Relation::WhereChain.include ActiverecordAnyOf::Chained
Loading

0 comments on commit 2fd8dec

Please sign in to comment.