where_any 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8720b83377ca8b8f8b55981a951157a5d800e4029fc61ec0786036e47915c841
4
+ data.tar.gz: 8c7db28c73611593239ba7385d8b51be63e3c8ff5da25392db8e323276a67e99
5
+ SHA512:
6
+ metadata.gz: f112f2ba41d11a6db21f6636eb820bf2731a2b8691cf232d2389ce4d73b9a4094a6d6749538f47cd553a96d9c1fd8b6878ca86343341d08deb673ed208760a28
7
+ data.tar.gz: '03830003bbeb718a747906b12d718780c74b9aa0c1e2be07c6a96013f0265dd47cbcf8e0acab852d5b971bf7db92c39e4f0a76218f03d1c507d8496f50edb8aa'
@@ -0,0 +1,18 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.1
14
+ bundler-cache: true
15
+ - name: Rubocop
16
+ run: bin/rubocop
17
+ - name: RSpec
18
+ run: bin/rspec
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.5
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+
9
+ Layout/LineLength:
10
+ Max: 120
11
+
12
+ Style/Documentation:
13
+ Enabled: false
14
+
15
+ Style/StringLiterals:
16
+ Enabled: true
17
+ EnforcedStyle: single_quotes
18
+
19
+ Style/StringLiteralsInInterpolation:
20
+ Enabled: true
21
+ EnforcedStyle: double_quotes
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in where_any_of.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.0'
10
+ gem 'rubocop', '~> 1.23'
11
+ gem 'rubocop-rspec', '~> 2.6'
data/Gemfile.lock ADDED
@@ -0,0 +1,77 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ where_any (0.1.0)
5
+ activerecord (>= 5.2.0, < 8)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.1.4.1)
11
+ activesupport (= 6.1.4.1)
12
+ activerecord (6.1.4.1)
13
+ activemodel (= 6.1.4.1)
14
+ activesupport (= 6.1.4.1)
15
+ activesupport (6.1.4.1)
16
+ concurrent-ruby (~> 1.0, >= 1.0.2)
17
+ i18n (>= 1.6, < 2)
18
+ minitest (>= 5.1)
19
+ tzinfo (~> 2.0)
20
+ zeitwerk (~> 2.3)
21
+ ast (2.4.2)
22
+ concurrent-ruby (1.1.9)
23
+ diff-lcs (1.4.4)
24
+ i18n (1.8.11)
25
+ concurrent-ruby (~> 1.0)
26
+ minitest (5.14.4)
27
+ parallel (1.21.0)
28
+ parser (3.0.3.2)
29
+ ast (~> 2.4.1)
30
+ rainbow (3.0.0)
31
+ rake (13.0.6)
32
+ regexp_parser (2.2.0)
33
+ rexml (3.2.5)
34
+ rspec (3.10.0)
35
+ rspec-core (~> 3.10.0)
36
+ rspec-expectations (~> 3.10.0)
37
+ rspec-mocks (~> 3.10.0)
38
+ rspec-core (3.10.1)
39
+ rspec-support (~> 3.10.0)
40
+ rspec-expectations (3.10.1)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.10.0)
43
+ rspec-mocks (3.10.2)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (~> 3.10.0)
46
+ rspec-support (3.10.3)
47
+ rubocop (1.23.0)
48
+ parallel (~> 1.10)
49
+ parser (>= 3.0.0.0)
50
+ rainbow (>= 2.2.2, < 4.0)
51
+ regexp_parser (>= 1.8, < 3.0)
52
+ rexml
53
+ rubocop-ast (>= 1.12.0, < 2.0)
54
+ ruby-progressbar (~> 1.7)
55
+ unicode-display_width (>= 1.4.0, < 3.0)
56
+ rubocop-ast (1.15.0)
57
+ parser (>= 3.0.1.1)
58
+ rubocop-rspec (2.6.0)
59
+ rubocop (~> 1.19)
60
+ ruby-progressbar (1.11.0)
61
+ tzinfo (2.0.4)
62
+ concurrent-ruby (~> 1.0)
63
+ unicode-display_width (2.1.0)
64
+ zeitwerk (2.5.1)
65
+
66
+ PLATFORMS
67
+ x86_64-linux
68
+
69
+ DEPENDENCIES
70
+ rake (~> 13.0)
71
+ rspec (~> 3.0)
72
+ rubocop (~> 1.23)
73
+ rubocop-rspec (~> 2.6)
74
+ where_any!
75
+
76
+ BUNDLED WITH
77
+ 2.2.17
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # Where Any
2
+
3
+ Helpers for using the PostgreSQL `ANY()` and `ALL()` expressions in ActiveRecord queries. This provides the functionality of WHERE IN, but in a more prepared statement friendly way.
4
+
5
+ Tested and validated only for PostgreSQL.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'where_any'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install where_any
22
+
23
+ Then in any of your models:
24
+
25
+ ```ruby
26
+ class User < ApplicationRecord
27
+ extend WhereAny
28
+
29
+ # ...
30
+ end
31
+ ```
32
+
33
+ Or, to install these helpers for your entire application:
34
+
35
+ ```ruby
36
+ class ApplicationRecord < ActiveRecord::Base
37
+ self.abstract_class = true
38
+
39
+ extend WhereAny
40
+
41
+ # ...
42
+ end
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ To make a where `ANY()` query, use the `where_any` method:
48
+
49
+ ```ruby
50
+ User.where_any(:id, [1, 2, 3, 4, 5])
51
+ ```
52
+
53
+ The first argument of `where_any` refers to the column being tested, and the second argument is a list of values to include in the condition.
54
+
55
+ Which would produce the following SQL:
56
+
57
+ ```sql
58
+ SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1) [["id", "{1,2,3,4,5}"]
59
+ ```
60
+
61
+ It's also possible to construct a negated where `ANY()` query, like so:
62
+
63
+ ```ruby
64
+ User.where_none(:id, [1, 2, 3, 4, 5])
65
+ ```
66
+
67
+ Which would produce the following SQL:
68
+
69
+ ```sql
70
+ SELECT "users".* FROM "users" WHERE "users"."id" != ALL($1) [["id", "{1,2,3,4,5}"]]
71
+ ```
72
+
73
+ ### Where ANY vs. Where IN
74
+
75
+ The advantage of using where `where_any` over ActiveRecord's built-in WHERE IN support is that it produces the same SQL statement regardless of the number of elements supplied. This is advantageous when using prepared statements, as the same statement can be reused regardless of the number of inputs supplied.
76
+
77
+ Consider for example:
78
+
79
+ ```ruby
80
+ User.where(id: [1, 2, 3])
81
+ User.where(id: [1, 2, 3, 4])
82
+ User.where(id: [1, 2, 3, 4, 5])
83
+
84
+ # Versus
85
+
86
+ User.where_any(:id, [1, 2, 3])
87
+ User.where_any(:id, [1, 2, 3, 4])
88
+ User.where_any(:id, [1, 2, 3, 4, 5])
89
+ ```
90
+
91
+ These sets of queries produce the following sets of SQL respectively:
92
+
93
+ ```sql
94
+ SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3) [["id", 1], ["id", 2], ["id", 3]]
95
+ SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3, $4) [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
96
+ SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3, $4, $5) [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5]]
97
+
98
+ -- Versus
99
+
100
+ SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1) [["id", "{1,2,3}"]]
101
+ SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1) [["id", "{1,2,3,4}"]]
102
+ SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1) [["id", "{1,2,3,4,5}"]]
103
+ ```
104
+
105
+ Using the `ANY()` notation allows us to reuse the same query with the same number of parameter binds regardless of what number of inputs are supplied.
106
+
107
+ ### Performance
108
+
109
+ Using modern version of Postgres, there is no disadvantage to using `ANY()` from a query plan perspective.
110
+
111
+ Here is an example query plan when using `where_any()`:
112
+
113
+ ```sql
114
+ EXPLAIN for: SELECT "users".* FROM "users" WHERE "users"."id" = ANY($1) [["id", "{1,2,3,4,5}"]]
115
+ QUERY PLAN
116
+ ----------------------------------------------------------------------------
117
+ Index Scan using users_pkey on users
118
+ Index Cond: (id = ANY ('{1,2,3,4,5}'::integer[]))
119
+ (2 rows)
120
+ ```
121
+
122
+ And here is the query plan when using ActiveRecord's WHERE IN:
123
+
124
+ ```sql
125
+ EXPLAIN for: SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3, $4, $5) [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5]]
126
+ QUERY PLAN
127
+ ----------------------------------------------------------------------------
128
+ Index Scan using users_pkey on users
129
+ Index Cond: (id = ANY ('{1,2,3,4,5}'::integer[]))
130
+ (2 rows)
131
+ ```
132
+
133
+ Note how these two queries produced the exact same query plan. According to the PostgreSQL manual, these operations are equivalent:
134
+ https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-ANY-SOME
135
+
136
+ ## Development
137
+
138
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
139
+
140
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
141
+
142
+ ## Contributing
143
+
144
+ Bug reports and pull requests are welcome on GitHub at https://github.com/thriver/where_any.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'where_any'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhereAny
4
+ VERSION = '0.1.0'
5
+ end
data/lib/where_any.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'where_any/version'
4
+
5
+ module WhereAny
6
+ # @param name [String]
7
+ # @param value [Object]
8
+ # @return [Arel::Nodes::Node]
9
+ def bind_param(name, value)
10
+ attribute_type = type_for_attribute(name)
11
+ query_attribute = ActiveRecord::Relation::QueryAttribute.new(name.to_s, value, attribute_type)
12
+
13
+ Arel::Nodes::BindParam.new(query_attribute)
14
+ end
15
+
16
+ # @param name [String]
17
+ # @param value [Object]
18
+ # @return [Arel::Nodes::Node]
19
+ def bind_array(name, value)
20
+ element_type = type_for_attribute(name)
21
+ attribute_type = ActiveRecord::Type.lookup(element_type.type, array: true)
22
+ query_attribute = ActiveRecord::Relation::QueryAttribute.new(name.to_s, value, attribute_type)
23
+
24
+ Arel::Nodes::BindParam.new(query_attribute)
25
+ end
26
+
27
+ # @param column [String, Symbol]
28
+ # @param values [Array<Object>]
29
+ # @return [ActiveRecord::Relation]
30
+ def where_any(column, values)
31
+ return none if values.blank?
32
+
33
+ arel_column = arel_table[column]
34
+ any_of_values = Arel::Nodes::NamedFunction.new('ANY', [bind_array(column, values)])
35
+
36
+ where(arel_column.eq(any_of_values))
37
+ end
38
+
39
+ # @param column [String, Symbol]
40
+ # @param values [Array<Object>]
41
+ # @return [ActiveRecord::Relation]
42
+ def where_none(column, values)
43
+ return all if values.blank?
44
+
45
+ arel_column = arel_table[column]
46
+ all_of_values = Arel::Nodes::NamedFunction.new('ALL', [bind_array(column, values)])
47
+
48
+ where(arel_column.not_eq(all_of_values))
49
+ end
50
+ end
data/where_any.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/where_any/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'where_any'
7
+ spec.version = WhereAny::VERSION
8
+ spec.authors = ['Minty Fresh']
9
+ spec.email = ['7896757+mintyfresh@users.noreply.github.com']
10
+
11
+ spec.summary = 'Postgres ANY() and ALL() expressions for ActiveRecord.'
12
+ spec.description = spec.summary
13
+ spec.homepage = 'https://github.com/thriver/where_any'
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
15
+
16
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
17
+ spec.metadata['rubygems_mfa_required'] = 'true'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
+ end
27
+
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ spec.add_dependency 'activerecord', '>= 5.2.0', '< 8'
34
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: where_any
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Minty Fresh
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 5.2.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8'
33
+ description: Postgres ANY() and ALL() expressions for ActiveRecord.
34
+ email:
35
+ - 7896757+mintyfresh@users.noreply.github.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - ".github/workflows/main.yml"
41
+ - ".gitignore"
42
+ - ".rspec"
43
+ - ".rubocop.yml"
44
+ - Gemfile
45
+ - Gemfile.lock
46
+ - README.md
47
+ - Rakefile
48
+ - bin/console
49
+ - bin/rspec
50
+ - bin/rubocop
51
+ - bin/setup
52
+ - lib/where_any.rb
53
+ - lib/where_any/version.rb
54
+ - where_any.gemspec
55
+ homepage: https://github.com/thriver/where_any
56
+ licenses: []
57
+ metadata:
58
+ allowed_push_host: https://rubygems.org/
59
+ rubygems_mfa_required: 'true'
60
+ homepage_uri: https://github.com/thriver/where_any
61
+ source_code_uri: https://github.com/thriver/where_any
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 2.5.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.2.15
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Postgres ANY() and ALL() expressions for ActiveRecord.
81
+ test_files: []