n_plus_one_control 0.2.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +1 -1
- data/README.md +127 -12
- data/lib/n_plus_one_control.rb +96 -5
- data/lib/n_plus_one_control/executor.rb +63 -23
- data/lib/n_plus_one_control/minitest.rb +24 -5
- data/lib/n_plus_one_control/railtie.rb +13 -0
- data/lib/n_plus_one_control/rspec.rb +4 -1
- data/lib/n_plus_one_control/rspec/context.rb +12 -11
- data/lib/n_plus_one_control/rspec/dsl.rb +24 -6
- data/lib/n_plus_one_control/rspec/matcher.rb +13 -5
- data/lib/n_plus_one_control/version.rb +1 -1
- metadata +19 -89
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -74
- data/.travis.yml +0 -5
- data/Gemfile +0 -4
- data/Rakefile +0 -13
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/n_plus_one_control.gemspec +0 -47
- data/spec/n_plus_one_control/executor_spec.rb +0 -65
- data/spec/n_plus_one_control/rspec_spec.rb +0 -84
- data/spec/n_plus_one_control_spec.rb +0 -9
- data/spec/spec_helper.rb +0 -30
- data/spec/support/post.rb +0 -19
- data/spec/support/user.rb +0 -17
- data/tests/minitest_test.rb +0 -63
- data/tests/test_helper.rb +0 -32
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
require "bundler/gem_tasks"
|
2
|
-
require "rspec/core/rake_task"
|
3
|
-
require "rubocop/rake_task"
|
4
|
-
require "rake/testtask"
|
5
|
-
|
6
|
-
Rake::TestTask.new do |t|
|
7
|
-
t.test_files = FileList['tests/**/*_test.rb']
|
8
|
-
end
|
9
|
-
|
10
|
-
RuboCop::RakeTask.new
|
11
|
-
RSpec::Core::RakeTask.new(:spec)
|
12
|
-
|
13
|
-
task :default => [:spec, :test, :rubocop]
|
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "n_plus_one_control"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|
data/bin/setup
DELETED
data/n_plus_one_control.gemspec
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'n_plus_one_control/version'
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "n_plus_one_control"
|
8
|
-
spec.version = NPlusOneControl::VERSION
|
9
|
-
spec.authors = ["palkan"]
|
10
|
-
spec.email = ["dementiev.vm@gmail.com"]
|
11
|
-
|
12
|
-
spec.summary = "RSpec and Minitest matchers to prevent N+1 queries problem"
|
13
|
-
spec.required_ruby_version = '>= 2.0.0'
|
14
|
-
spec.description = %{
|
15
|
-
RSpec and Minitest matchers to prevent N+1 queries problem.
|
16
|
-
|
17
|
-
Evaluates code under consideration several times with different scale factors
|
18
|
-
to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
|
19
|
-
|
20
|
-
Example:
|
21
|
-
|
22
|
-
```ruby
|
23
|
-
context "N+1", :n_plus_one do
|
24
|
-
populate { |n| create_list(:post, n) }
|
25
|
-
|
26
|
-
specify do
|
27
|
-
expect { get :index }.to perform_constant_number_of_queries
|
28
|
-
end
|
29
|
-
end
|
30
|
-
```
|
31
|
-
}
|
32
|
-
spec.homepage = "http://github.com/palkan/n_plus_one_control"
|
33
|
-
spec.license = "MIT"
|
34
|
-
|
35
|
-
spec.files = `git ls-files`.split($/)
|
36
|
-
spec.require_paths = ["lib"]
|
37
|
-
|
38
|
-
spec.add_development_dependency "bundler", "~> 1.10"
|
39
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
40
|
-
spec.add_development_dependency "rspec", "~> 3.5"
|
41
|
-
spec.add_development_dependency "minitest", "~> 5.9"
|
42
|
-
spec.add_development_dependency "factory_girl", "~> 4.8.0"
|
43
|
-
spec.add_development_dependency "rubocop", "~> 0.49"
|
44
|
-
spec.add_development_dependency "activerecord", "~> 5.1"
|
45
|
-
spec.add_development_dependency "sqlite3"
|
46
|
-
spec.add_development_dependency "pry-byebug"
|
47
|
-
end
|
@@ -1,65 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "spec_helper"
|
4
|
-
|
5
|
-
describe NPlusOneControl::Executor do
|
6
|
-
let(:populate) do
|
7
|
-
->(n) { create_list(:post, n) }
|
8
|
-
end
|
9
|
-
|
10
|
-
let(:observable) do
|
11
|
-
-> { Post.find_each(&:user) }
|
12
|
-
end
|
13
|
-
|
14
|
-
it "raises when block is missing" do
|
15
|
-
expect { described_class.call(population: populate) }
|
16
|
-
.to raise_error(ArgumentError, "Block is required!")
|
17
|
-
end
|
18
|
-
|
19
|
-
it "raises when populate is missing" do
|
20
|
-
expect { described_class.call(&observable) }
|
21
|
-
.to raise_error(ArgumentError, /population/)
|
22
|
-
end
|
23
|
-
|
24
|
-
it "returns correct counts for default scales" do
|
25
|
-
result = described_class.call(
|
26
|
-
population: populate,
|
27
|
-
&observable
|
28
|
-
)
|
29
|
-
|
30
|
-
expect(result.size).to eq 2
|
31
|
-
expect(result.first[0]).to eq 2
|
32
|
-
expect(result.first[1].size).to eq 3
|
33
|
-
expect(result.last[0]).to eq 3
|
34
|
-
expect(result.last[1].size).to eq 4
|
35
|
-
end
|
36
|
-
|
37
|
-
it "returns correct counts for custom scales" do
|
38
|
-
result = described_class.call(
|
39
|
-
population: populate,
|
40
|
-
scale_factors: [5, 10, 100],
|
41
|
-
&observable
|
42
|
-
)
|
43
|
-
|
44
|
-
expect(result.size).to eq 3
|
45
|
-
expect(result.first[0]).to eq 5
|
46
|
-
expect(result.first[1].size).to eq 6
|
47
|
-
expect(result.second[0]).to eq 10
|
48
|
-
expect(result.second[1].size).to eq 11
|
49
|
-
expect(result.last[0]).to eq 100
|
50
|
-
expect(result.last[1].size).to eq 101
|
51
|
-
end
|
52
|
-
|
53
|
-
it "returns correct counts with custom match" do
|
54
|
-
result = described_class.call(
|
55
|
-
population: populate,
|
56
|
-
matching: /users/,
|
57
|
-
&observable
|
58
|
-
)
|
59
|
-
|
60
|
-
expect(result.first[0]).to eq 2
|
61
|
-
expect(result.first[1].size).to eq 2
|
62
|
-
expect(result.last[0]).to eq 3
|
63
|
-
expect(result.last[1].size).to eq 3
|
64
|
-
end
|
65
|
-
end
|
@@ -1,84 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "spec_helper"
|
4
|
-
|
5
|
-
describe NPlusOneControl::RSpec do
|
6
|
-
context "when no N+1", :n_plus_one do
|
7
|
-
populate { |n| create_list(:post, n) }
|
8
|
-
|
9
|
-
specify do
|
10
|
-
expect { Post.preload(:user).find_each { |p| p.user.name } }
|
11
|
-
.to perform_constant_number_of_queries
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
context "when has N+1", :n_plus_one do
|
16
|
-
populate { |n| create_list(:post, n) }
|
17
|
-
|
18
|
-
specify do
|
19
|
-
expect do
|
20
|
-
expect { Post.find_each { |p| p.user.name } }
|
21
|
-
.to perform_constant_number_of_queries
|
22
|
-
end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
context "when context is missing" do
|
27
|
-
specify do
|
28
|
-
expect do
|
29
|
-
expect { subject }.to perform_constant_number_of_queries
|
30
|
-
end.to raise_error(/missing tag/i)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
context "when populate is missing", :n_plus_one do
|
35
|
-
specify do
|
36
|
-
expect do
|
37
|
-
expect { subject }.to perform_constant_number_of_queries
|
38
|
-
end.to raise_error(/please provide populate/i)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
context "when negated" do
|
43
|
-
specify do
|
44
|
-
expect do
|
45
|
-
expect { subject }.not_to perform_constant_number_of_queries
|
46
|
-
end.to raise_error(/support negation/i)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
context "when verbose", :n_plus_one do
|
51
|
-
populate { |n| create_list(:post, n) }
|
52
|
-
|
53
|
-
around(:each) do |ex|
|
54
|
-
NPlusOneControl.verbose = true
|
55
|
-
ex.run
|
56
|
-
NPlusOneControl.verbose = false
|
57
|
-
end
|
58
|
-
|
59
|
-
specify do
|
60
|
-
expect do
|
61
|
-
expect { Post.find_each { |p| p.user.name } }
|
62
|
-
.to perform_constant_number_of_queries
|
63
|
-
end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /select .+ from/i)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
context "with scale_factors", :n_plus_one do
|
68
|
-
populate { |n| create_list(:post, n) }
|
69
|
-
|
70
|
-
specify do
|
71
|
-
expect { Post.find_each { |p| p.user.name } }
|
72
|
-
.to perform_constant_number_of_queries.with_scale_factors(1, 1)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
context "with matching", :n_plus_one do
|
77
|
-
populate { |n| create_list(:post, n) }
|
78
|
-
|
79
|
-
specify do
|
80
|
-
expect { Post.find_each { |p| p.user.name } }
|
81
|
-
.to perform_constant_number_of_queries.matching(/posts/)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
data/spec/spec_helper.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
4
|
-
require "n_plus_one_control/rspec"
|
5
|
-
require "benchmark"
|
6
|
-
require "active_record"
|
7
|
-
require "factory_girl"
|
8
|
-
require "pry-byebug"
|
9
|
-
|
10
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
11
|
-
|
12
|
-
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
13
|
-
|
14
|
-
RSpec.configure do |config|
|
15
|
-
config.mock_with :rspec
|
16
|
-
|
17
|
-
config.order = :random
|
18
|
-
config.filter_run focus: true
|
19
|
-
config.run_all_when_everything_filtered = true
|
20
|
-
|
21
|
-
config.include FactoryGirl::Syntax::Methods
|
22
|
-
|
23
|
-
config.before(:each) do
|
24
|
-
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
25
|
-
end
|
26
|
-
|
27
|
-
config.after(:each) do
|
28
|
-
ActiveRecord::Base.connection.rollback_transaction
|
29
|
-
end
|
30
|
-
end
|
data/spec/support/post.rb
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
ActiveRecord::Schema.define do
|
4
|
-
create_table :posts do |t|
|
5
|
-
t.string :title
|
6
|
-
t.integer :user_id
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
class Post < ActiveRecord::Base
|
11
|
-
belongs_to :user
|
12
|
-
end
|
13
|
-
|
14
|
-
FactoryGirl.define do
|
15
|
-
factory :post do
|
16
|
-
title "Title"
|
17
|
-
user
|
18
|
-
end
|
19
|
-
end
|
data/spec/support/user.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
ActiveRecord::Schema.define do
|
4
|
-
create_table :users do |t|
|
5
|
-
t.string :name
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
class User < ActiveRecord::Base
|
10
|
-
has_many :posts
|
11
|
-
end
|
12
|
-
|
13
|
-
FactoryGirl.define do
|
14
|
-
factory :user do
|
15
|
-
name "John"
|
16
|
-
end
|
17
|
-
end
|
data/tests/minitest_test.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "test_helper"
|
4
|
-
|
5
|
-
class TestMinitest < Minitest::Test
|
6
|
-
def test_no_n_plus_one_error
|
7
|
-
populate = ->(n) { create_list(:post, n) }
|
8
|
-
|
9
|
-
assert_perform_constant_number_of_queries(populate: populate) do
|
10
|
-
Post.preload(:user).find_each { |p| p.user.name }
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_with_n_plus_one_error
|
15
|
-
populate = ->(n) { create_list(:post, n) }
|
16
|
-
|
17
|
-
e = assert_raises Minitest::Assertion do
|
18
|
-
assert_perform_constant_number_of_queries(populate: populate) do
|
19
|
-
Post.find_each { |p| p.user.name }
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
assert_match "Expected to make the same number of queries", e.message
|
24
|
-
assert_match "3 for N=2", e.message
|
25
|
-
assert_match "4 for N=3", e.message
|
26
|
-
end
|
27
|
-
|
28
|
-
def test_no_n_plus_one_error_with_scale_factors
|
29
|
-
populate = ->(n) { create_list(:post, n) }
|
30
|
-
|
31
|
-
assert_perform_constant_number_of_queries(
|
32
|
-
populate: populate,
|
33
|
-
scale_factors: [1, 1]
|
34
|
-
) do
|
35
|
-
Post.find_each { |p| p.user.name }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def test_no_n_plus_one_error_with_matching
|
40
|
-
populate = ->(n) { create_list(:post, n) }
|
41
|
-
|
42
|
-
assert_perform_constant_number_of_queries(
|
43
|
-
populate: populate,
|
44
|
-
matching: /posts/
|
45
|
-
) do
|
46
|
-
Post.find_each { |p| p.user.name }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def populate(n)
|
51
|
-
create_list(:post, n)
|
52
|
-
end
|
53
|
-
|
54
|
-
def test_fallback_to_populate_method
|
55
|
-
e = assert_raises Minitest::Assertion do
|
56
|
-
assert_perform_constant_number_of_queries do
|
57
|
-
Post.find_each { |p| p.user.name }
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
assert_match "Expected to make the same number of queries", e.message
|
62
|
-
end
|
63
|
-
end
|
data/tests/test_helper.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "minitest/autorun"
|
4
|
-
require "minitest/pride"
|
5
|
-
|
6
|
-
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
7
|
-
Thread.abort_on_exception = true
|
8
|
-
|
9
|
-
require "n_plus_one_control/minitest"
|
10
|
-
require "benchmark"
|
11
|
-
require "active_record"
|
12
|
-
require "factory_girl"
|
13
|
-
require "pry-byebug"
|
14
|
-
|
15
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
16
|
-
|
17
|
-
Dir["#{File.dirname(__FILE__)}/../spec/support/**/*.rb"].each { |f| require f }
|
18
|
-
|
19
|
-
module TransactionalTests
|
20
|
-
def setup
|
21
|
-
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
22
|
-
super
|
23
|
-
end
|
24
|
-
|
25
|
-
def teardown
|
26
|
-
super
|
27
|
-
ActiveRecord::Base.connection.rollback_transaction
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
Minitest::Test.prepend TransactionalTests
|
32
|
-
Minitest::Test.include FactoryGirl::Syntax::Methods
|