n_plus_one_control 0.3.1 → 0.6.1
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 +41 -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 -24
- 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 +1 -10
- 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 +20 -86
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -78
- data/.travis.yml +0 -9
- 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 -35
- 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
|
@@ -3,17 +3,8 @@
|
|
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
|
|
@@ -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(&block)
|
13
|
+
return @warmup unless block
|
12
14
|
|
13
|
-
|
15
|
+
@warmup = block
|
16
|
+
end
|
17
|
+
|
18
|
+
# Setup populate callback, which is used
|
19
|
+
# to prepare data for each run.
|
20
|
+
def populate(&block)
|
21
|
+
return @populate unless block
|
22
|
+
|
23
|
+
@populate = block
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-05 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,62 +80,6 @@ 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.61.0
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - "~>"
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 0.61.0
|
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: 1.3.6
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - "~>"
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: 1.3.6
|
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
|
@@ -146,37 +90,28 @@ executables: []
|
|
146
90
|
extensions: []
|
147
91
|
extra_rdoc_files: []
|
148
92
|
files:
|
149
|
-
-
|
150
|
-
- ".rspec"
|
151
|
-
- ".rubocop.yml"
|
152
|
-
- ".travis.yml"
|
153
|
-
- Gemfile
|
93
|
+
- CHANGELOG.md
|
154
94
|
- LICENSE.txt
|
155
95
|
- README.md
|
156
|
-
- Rakefile
|
157
|
-
- bin/console
|
158
|
-
- bin/setup
|
159
96
|
- lib/n_plus_one_control.rb
|
160
97
|
- lib/n_plus_one_control/executor.rb
|
161
98
|
- lib/n_plus_one_control/minitest.rb
|
99
|
+
- lib/n_plus_one_control/railtie.rb
|
162
100
|
- lib/n_plus_one_control/rspec.rb
|
163
101
|
- lib/n_plus_one_control/rspec/context.rb
|
164
102
|
- lib/n_plus_one_control/rspec/dsl.rb
|
165
|
-
- 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
|
166
105
|
- lib/n_plus_one_control/version.rb
|
167
|
-
- n_plus_one_control.gemspec
|
168
|
-
- spec/n_plus_one_control/executor_spec.rb
|
169
|
-
- spec/n_plus_one_control/rspec_spec.rb
|
170
|
-
- spec/n_plus_one_control_spec.rb
|
171
|
-
- spec/spec_helper.rb
|
172
|
-
- spec/support/post.rb
|
173
|
-
- spec/support/user.rb
|
174
|
-
- tests/minitest_test.rb
|
175
|
-
- tests/test_helper.rb
|
176
106
|
homepage: http://github.com/palkan/n_plus_one_control
|
177
107
|
licenses:
|
178
108
|
- MIT
|
179
|
-
metadata:
|
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
|
180
115
|
post_install_message:
|
181
116
|
rdoc_options: []
|
182
117
|
require_paths:
|
@@ -185,15 +120,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
185
120
|
requirements:
|
186
121
|
- - ">="
|
187
122
|
- !ruby/object:Gem::Version
|
188
|
-
version: 2.
|
123
|
+
version: 2.5.0
|
189
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
190
125
|
requirements:
|
191
126
|
- - ">="
|
192
127
|
- !ruby/object:Gem::Version
|
193
128
|
version: '0'
|
194
129
|
requirements: []
|
195
|
-
|
196
|
-
rubygems_version: 2.7.6
|
130
|
+
rubygems_version: 3.0.6
|
197
131
|
signing_key:
|
198
132
|
specification_version: 4
|
199
133
|
summary: RSpec and Minitest matchers to prevent N+1 queries problem
|