n_plus_one_control 0.3.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +33 -0
- data/LICENSE.txt +1 -1
- data/README.md +107 -15
- data/lib/n_plus_one_control.rb +102 -6
- data/lib/n_plus_one_control/executor.rb +63 -23
- data/lib/n_plus_one_control/minitest.rb +54 -9
- data/lib/n_plus_one_control/railtie.rb +13 -0
- data/lib/n_plus_one_control/rspec.rb +6 -2
- data/lib/n_plus_one_control/rspec/context.rb +7 -11
- data/lib/n_plus_one_control/rspec/dsl.rb +21 -12
- data/lib/n_plus_one_control/rspec/{matcher.rb → matchers/perform_constant_number_of_queries.rb} +6 -6
- data/lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb +53 -0
- data/lib/n_plus_one_control/version.rb +1 -1
- metadata +24 -93
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -77
- 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 -113
- 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
@@ -5,10 +5,6 @@ require "n_plus_one_control"
|
|
5
5
|
module NPlusOneControl
|
6
6
|
# Minitest assertions
|
7
7
|
module MinitestHelper
|
8
|
-
def warming_up(warmup)
|
9
|
-
(warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
|
10
|
-
end
|
11
|
-
|
12
8
|
def assert_perform_constant_number_of_queries(
|
13
9
|
populate: nil,
|
14
10
|
matching: nil,
|
@@ -20,15 +16,64 @@ module NPlusOneControl
|
|
20
16
|
|
21
17
|
warming_up warmup
|
22
18
|
|
23
|
-
|
24
|
-
population: populate ||
|
25
|
-
matching: matching ||
|
19
|
+
@executor = NPlusOneControl::Executor.new(
|
20
|
+
population: populate || population_method,
|
21
|
+
matching: matching || NPlusOneControl.default_matching,
|
26
22
|
scale_factors: scale_factors || NPlusOneControl.default_scale_factors
|
27
|
-
)
|
23
|
+
)
|
24
|
+
|
25
|
+
queries = @executor.call { yield }
|
28
26
|
|
29
27
|
counts = queries.map(&:last).map(&:size)
|
30
28
|
|
31
|
-
assert counts.max == counts.min, NPlusOneControl.failure_message(queries)
|
29
|
+
assert counts.max == counts.min, NPlusOneControl.failure_message(:constant_queries, queries)
|
30
|
+
end
|
31
|
+
|
32
|
+
def assert_perform_linear_number_of_queries(
|
33
|
+
slope: 1,
|
34
|
+
populate: nil,
|
35
|
+
matching: nil,
|
36
|
+
scale_factors: nil,
|
37
|
+
warmup: nil
|
38
|
+
)
|
39
|
+
|
40
|
+
raise ArgumentError, "Block is required" unless block_given?
|
41
|
+
|
42
|
+
warming_up warmup
|
43
|
+
|
44
|
+
@executor = NPlusOneControl::Executor.new(
|
45
|
+
population: populate || population_method,
|
46
|
+
matching: matching || NPlusOneControl.default_matching,
|
47
|
+
scale_factors: scale_factors || NPlusOneControl.default_scale_factors
|
48
|
+
)
|
49
|
+
|
50
|
+
queries = @executor.call { yield }
|
51
|
+
|
52
|
+
assert linear?(queries, slope: slope), NPlusOneControl.failure_message(:linear_queries, queries)
|
53
|
+
end
|
54
|
+
|
55
|
+
def current_scale
|
56
|
+
@executor&.current_scale
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def warming_up(warmup)
|
62
|
+
(warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
|
63
|
+
end
|
64
|
+
|
65
|
+
def population_method
|
66
|
+
methods.include?(:populate) ? method(:populate) : nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def linear?(queries, slope:)
|
70
|
+
queries.each_cons(2).all? do |pair|
|
71
|
+
scales = pair.map(&:first)
|
72
|
+
query_lists = pair.map(&:last)
|
73
|
+
|
74
|
+
actual_slope = (query_lists[1].size - query_lists[0].size) / (scales[1] - scales[0])
|
75
|
+
actual_slope <= slope
|
76
|
+
end
|
32
77
|
end
|
33
78
|
end
|
34
79
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NPlusOneControl # :nodoc:
|
4
|
+
class Railtie < ::Rails::Railtie # :nodoc:
|
5
|
+
initializer "n_plus_one_control.backtrace_cleaner" do
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
7
|
+
NPlusOneControl.backtrace_cleaner = lambda do |locations|
|
8
|
+
::Rails.backtrace_cleaner.clean(locations)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
gem "rspec-core", ">= 3.5"
|
4
|
+
|
3
5
|
require "n_plus_one_control"
|
4
6
|
require "n_plus_one_control/rspec/dsl"
|
5
|
-
require "n_plus_one_control/rspec/
|
7
|
+
require "n_plus_one_control/rspec/matchers/perform_constant_number_of_queries"
|
8
|
+
require "n_plus_one_control/rspec/matchers/perform_linear_number_of_queries"
|
6
9
|
require "n_plus_one_control/rspec/context"
|
7
10
|
|
8
11
|
module NPlusOneControl
|
@@ -11,5 +14,6 @@ module NPlusOneControl
|
|
11
14
|
end
|
12
15
|
|
13
16
|
::RSpec.configure do |config|
|
14
|
-
config.extend NPlusOneControl::RSpec::DSL, n_plus_one: true
|
17
|
+
config.extend NPlusOneControl::RSpec::DSL::ClassMethods, n_plus_one: true
|
18
|
+
config.include NPlusOneControl::RSpec::DSL, n_plus_one: true
|
15
19
|
end
|
@@ -1,24 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
RSpec.shared_context "n_plus_one_control" do
|
4
4
|
# Helper to access populate block from within example/matcher
|
5
5
|
let(:n_plus_one_populate) do |ex|
|
6
|
-
if ex.example_group.populate.nil?
|
7
|
-
raise(
|
8
|
-
<<-MSG
|
9
|
-
Populate block is missing!
|
6
|
+
return if ex.example_group.populate.nil?
|
10
7
|
|
11
|
-
Please provide populate callback, e.g.:
|
12
|
-
|
13
|
-
populate { |n| n.times { create_some_stuff } }
|
14
|
-
MSG
|
15
|
-
)
|
16
|
-
end
|
17
8
|
->(n) { ex.instance_exec(n, &ex.example_group.populate) }
|
18
9
|
end
|
19
10
|
|
20
11
|
let(:n_plus_one_warmup) do |ex|
|
21
12
|
return if ex.example_group.warmup.nil?
|
13
|
+
|
22
14
|
-> { ex.instance_exec(&ex.example_group.warmup) }
|
23
15
|
end
|
24
16
|
end
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.include_context "n_plus_one_control", n_plus_one: true
|
20
|
+
end
|
@@ -2,23 +2,32 @@
|
|
2
2
|
|
3
3
|
module NPlusOneControl
|
4
4
|
module RSpec
|
5
|
-
#
|
5
|
+
# Includes scale method into RSpec Example
|
6
6
|
module DSL
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# Extends RSpec ExampleGroup with populate & warmup methods
|
8
|
+
module ClassMethods
|
9
|
+
# Setup warmup block, wich will run before matching
|
10
|
+
# for example, if using cache, then later queries
|
11
|
+
# will perform less DB queries than first
|
12
|
+
def warmup
|
13
|
+
return @warmup unless block_given?
|
12
14
|
|
13
|
-
|
15
|
+
@warmup = Proc.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Setup populate callback, which is used
|
19
|
+
# to prepare data for each run.
|
20
|
+
def populate
|
21
|
+
return @populate unless block_given?
|
22
|
+
|
23
|
+
@populate = Proc.new
|
24
|
+
end
|
14
25
|
end
|
15
26
|
|
16
|
-
|
17
|
-
# to prepare data for each run.
|
18
|
-
def populate
|
19
|
-
return @populate unless block_given?
|
27
|
+
attr_accessor :executor
|
20
28
|
|
21
|
-
|
29
|
+
def current_scale
|
30
|
+
executor&.current_scale
|
22
31
|
end
|
23
32
|
end
|
24
33
|
end
|
data/lib/n_plus_one_control/rspec/{matcher.rb → matchers/perform_constant_number_of_queries.rb}
RENAMED
@@ -27,16 +27,16 @@
|
|
27
27
|
|
28
28
|
warmup.call if warmup.present?
|
29
29
|
|
30
|
-
|
31
|
-
pattern = @pattern || /^SELECT/i
|
30
|
+
pattern = @pattern || NPlusOneControl.default_matching
|
32
31
|
|
33
|
-
@
|
32
|
+
@matcher_execution_context.executor = NPlusOneControl::Executor.new(
|
34
33
|
population: populate,
|
35
34
|
matching: pattern,
|
36
|
-
scale_factors: @factors
|
37
|
-
&actual
|
35
|
+
scale_factors: @factors
|
38
36
|
)
|
39
37
|
|
38
|
+
@queries = @matcher_execution_context.executor.call(&actual)
|
39
|
+
|
40
40
|
counts = @queries.map(&:last).map(&:size)
|
41
41
|
|
42
42
|
counts.max == counts.min
|
@@ -46,6 +46,6 @@
|
|
46
46
|
raise "This matcher doesn't support negation"
|
47
47
|
end
|
48
48
|
|
49
|
-
failure_message { |_actual| NPlusOneControl.failure_message(@queries) }
|
49
|
+
failure_message { |_actual| NPlusOneControl.failure_message(:constant_queries, @queries) }
|
50
50
|
end
|
51
51
|
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
::RSpec::Matchers.define :perform_linear_number_of_queries do |slope: 1|
|
5
|
+
supports_block_expectations
|
6
|
+
|
7
|
+
chain :with_scale_factors do |*factors|
|
8
|
+
@factors = factors
|
9
|
+
end
|
10
|
+
|
11
|
+
chain :matching do |pattern|
|
12
|
+
@pattern = pattern
|
13
|
+
end
|
14
|
+
|
15
|
+
chain :with_warming_up do
|
16
|
+
@warmup = true
|
17
|
+
end
|
18
|
+
|
19
|
+
match do |actual, *_args|
|
20
|
+
raise ArgumentError, "Block is required" unless actual.is_a? Proc
|
21
|
+
|
22
|
+
raise "Missing tag :n_plus_one" unless
|
23
|
+
@matcher_execution_context.respond_to?(:n_plus_one_populate)
|
24
|
+
|
25
|
+
populate = @matcher_execution_context.n_plus_one_populate
|
26
|
+
warmup = @warmup ? actual : @matcher_execution_context.n_plus_one_warmup
|
27
|
+
|
28
|
+
warmup.call if warmup.present?
|
29
|
+
|
30
|
+
@matcher_execution_context.executor = NPlusOneControl::Executor.new(
|
31
|
+
population: populate,
|
32
|
+
matching: nil,
|
33
|
+
scale_factors: @factors
|
34
|
+
)
|
35
|
+
|
36
|
+
@queries = @matcher_execution_context.executor.call(&actual)
|
37
|
+
|
38
|
+
@queries.each_cons(2).all? do |pair|
|
39
|
+
scales = pair.map(&:first)
|
40
|
+
query_lists = pair.map(&:last)
|
41
|
+
|
42
|
+
actual_slope = (query_lists[1].size - query_lists[0].size) / (scales[1] - scales[0])
|
43
|
+
actual_slope <= slope
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
match_when_negated do |_actual|
|
48
|
+
raise "This matcher doesn't support negation"
|
49
|
+
end
|
50
|
+
|
51
|
+
failure_message { |_actual| NPlusOneControl.failure_message(:linear_queries, @queries) }
|
52
|
+
end
|
53
|
+
# rubocop:enable Metrics/BlockLength
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: n_plus_one_control
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.10'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.10'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,107 +80,39 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 4.8.0
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rubocop
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - "~>"
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0.49'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - "~>"
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0.49'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: activerecord
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '5.1'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '5.1'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: sqlite3
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: pry-byebug
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
139
83
|
description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
|
140
84
|
\ Evaluates code under consideration several times with different scale factors\n
|
141
85
|
\ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead
|
142
|
-
of O(N)).\n
|
143
|
-
\ populate { |n| create_list(:post, n) }\n\n specify do\n expect
|
144
|
-
{ get :index }.to perform_constant_number_of_queries\n end\n end\n ```\n
|
145
|
-
\ "
|
86
|
+
of O(N)).\n "
|
146
87
|
email:
|
147
88
|
- dementiev.vm@gmail.com
|
148
89
|
executables: []
|
149
90
|
extensions: []
|
150
91
|
extra_rdoc_files: []
|
151
92
|
files:
|
152
|
-
-
|
153
|
-
- ".rspec"
|
154
|
-
- ".rubocop.yml"
|
155
|
-
- ".travis.yml"
|
156
|
-
- Gemfile
|
93
|
+
- CHANGELOG.md
|
157
94
|
- LICENSE.txt
|
158
95
|
- README.md
|
159
|
-
- Rakefile
|
160
|
-
- bin/console
|
161
|
-
- bin/setup
|
162
96
|
- lib/n_plus_one_control.rb
|
163
97
|
- lib/n_plus_one_control/executor.rb
|
164
98
|
- lib/n_plus_one_control/minitest.rb
|
99
|
+
- lib/n_plus_one_control/railtie.rb
|
165
100
|
- lib/n_plus_one_control/rspec.rb
|
166
101
|
- lib/n_plus_one_control/rspec/context.rb
|
167
102
|
- lib/n_plus_one_control/rspec/dsl.rb
|
168
|
-
- lib/n_plus_one_control/rspec/
|
103
|
+
- lib/n_plus_one_control/rspec/matchers/perform_constant_number_of_queries.rb
|
104
|
+
- lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb
|
169
105
|
- lib/n_plus_one_control/version.rb
|
170
|
-
- n_plus_one_control.gemspec
|
171
|
-
- spec/n_plus_one_control/executor_spec.rb
|
172
|
-
- spec/n_plus_one_control/rspec_spec.rb
|
173
|
-
- spec/n_plus_one_control_spec.rb
|
174
|
-
- spec/spec_helper.rb
|
175
|
-
- spec/support/post.rb
|
176
|
-
- spec/support/user.rb
|
177
|
-
- tests/minitest_test.rb
|
178
|
-
- tests/test_helper.rb
|
179
106
|
homepage: http://github.com/palkan/n_plus_one_control
|
180
107
|
licenses:
|
181
108
|
- MIT
|
182
|
-
metadata:
|
183
|
-
|
109
|
+
metadata:
|
110
|
+
bug_tracker_uri: http://github.com/palkan/n_plus_one_control/issues
|
111
|
+
changelog_uri: https://github.com/palkan/n_plus_one_control/blob/master/CHANGELOG.md
|
112
|
+
documentation_uri: http://github.com/palkan/n_plus_one_control
|
113
|
+
homepage_uri: http://github.com/palkan/n_plus_one_control
|
114
|
+
source_code_uri: http://github.com/palkan/n_plus_one_control
|
115
|
+
post_install_message:
|
184
116
|
rdoc_options: []
|
185
117
|
require_paths:
|
186
118
|
- lib
|
@@ -188,16 +120,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
188
120
|
requirements:
|
189
121
|
- - ">="
|
190
122
|
- !ruby/object:Gem::Version
|
191
|
-
version: 2.
|
123
|
+
version: 2.5.0
|
192
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
125
|
requirements:
|
194
126
|
- - ">="
|
195
127
|
- !ruby/object:Gem::Version
|
196
128
|
version: '0'
|
197
129
|
requirements: []
|
198
|
-
|
199
|
-
|
200
|
-
signing_key:
|
130
|
+
rubygems_version: 3.0.3
|
131
|
+
signing_key:
|
201
132
|
specification_version: 4
|
202
133
|
summary: RSpec and Minitest matchers to prevent N+1 queries problem
|
203
134
|
test_files: []
|