n_plus_one_control 0.4.1 → 0.6.2
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 -1
- data/README.md +83 -15
- data/lib/n_plus_one_control/executor.rb +20 -2
- data/lib/n_plus_one_control/minitest.rb +35 -2
- data/lib/n_plus_one_control/railtie.rb +13 -0
- data/lib/n_plus_one_control/rspec/dsl.rb +6 -6
- data/lib/n_plus_one_control/rspec/{matcher.rb → matchers/perform_constant_number_of_queries.rb} +3 -4
- data/lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb +53 -0
- data/lib/n_plus_one_control/rspec.rb +2 -1
- data/lib/n_plus_one_control/version.rb +1 -1
- data/lib/n_plus_one_control.rb +60 -9
- metadata +14 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95b1fada3d7db7a761650c8cd63b93b93a463dcdbfcf2df2b6e3ea25ac04355a
|
4
|
+
data.tar.gz: 3a3937ad74d66ea0bc0e56b50954c585c36c401b67d9af4dfd2a94e4202ae0be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ce963308cf99f1c836e7d883a71dbab0b22f8326692444aa3c6639dfc87065fd3e6866ee1af5b358b42cbc67af4a1640677fc051264c8ba6bc35a1604953bd2
|
7
|
+
data.tar.gz: f914d8e34d49f7ace51c872fde24487f216b481f39b8236b9c7d24bef26530ba5cbd1040b71d0b445a73f5769762e942fd3b00a7d4211a704fa9dcebf3bd34d9
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,34 @@
|
|
1
|
-
|
1
|
+
# Change log
|
2
|
+
|
3
|
+
## master
|
4
|
+
|
5
|
+
## 0.6.2 (2021-10-26)
|
6
|
+
|
7
|
+
- Fix .ignore setting (.ignore setting was ignored by the Collector ;-))
|
8
|
+
- Fix rspec matchers to allow expectations inside execution block
|
9
|
+
|
10
|
+
## 0.6.1 (2021-03-05)
|
11
|
+
|
12
|
+
- Ruby 3.0 compatibility. ([@palkan][])
|
13
|
+
|
14
|
+
## 0.6.0 (2020-11-27)
|
15
|
+
|
16
|
+
- Fix table stats summary when queries use backticks to surround table names ([@andrewhampton][])
|
17
|
+
- Add support to test for linear query. ([@caalberts][])
|
18
|
+
|
19
|
+
## 0.5.0 (2020-09-07)
|
20
|
+
|
21
|
+
- **Ruby 2.5+ is required**. ([@palkan][])
|
22
|
+
|
23
|
+
- Add support for multiple backtrace lines in verbose output. ([@palkan][])
|
24
|
+
|
25
|
+
Could be specified via `NPLUSONE_BACKTRACE` env var.
|
26
|
+
|
27
|
+
- Add `NPLUSONE_TRUNCATE` env var to truncate queries in verbose mode. ([@palkan][])
|
28
|
+
|
29
|
+
- Support passing default filter via `NPLUSONE_FILTER` env var. ([@palkan][])
|
30
|
+
|
31
|
+
- Add location tracing to SQLs in verbose mode. ([@palkan][])
|
2
32
|
|
3
33
|
## 0.4.1 (2020-09-04)
|
4
34
|
|
@@ -12,3 +42,5 @@
|
|
12
42
|
|
13
43
|
[@Earendil95]: https://github.com/Earendil95
|
14
44
|
[@palkan]: https://github.com/palkan
|
45
|
+
[@caalberts]: https://github.com/caalberts
|
46
|
+
[@andrewhampton]: https://github.com/andrewhampton
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
[](https://rubygems.org/gems/n_plus_one_control)
|
1
|
+
[](https://rubygems.org/gems/n_plus_one_control)
|
2
|
+

|
2
3
|
|
3
4
|
# N + 1 Control
|
4
5
|
|
@@ -31,7 +32,7 @@ Add this line to your application's Gemfile:
|
|
31
32
|
|
32
33
|
```ruby
|
33
34
|
group :test do
|
34
|
-
gem
|
35
|
+
gem "n_plus_one_control"
|
35
36
|
end
|
36
37
|
```
|
37
38
|
|
@@ -47,8 +48,6 @@ First, add NPlusOneControl to your `spec_helper.rb`:
|
|
47
48
|
|
48
49
|
```ruby
|
49
50
|
# spec_helper.rb
|
50
|
-
...
|
51
|
-
|
52
51
|
require "n_plus_one_control/rspec"
|
53
52
|
```
|
54
53
|
|
@@ -86,10 +85,10 @@ Availables modifiers:
|
|
86
85
|
```ruby
|
87
86
|
# You can specify the RegExp to filter queries.
|
88
87
|
# By default, it only considers SELECT queries.
|
89
|
-
expect {
|
88
|
+
expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)
|
90
89
|
|
91
90
|
# You can also provide custom scale factors
|
92
|
-
expect {
|
91
|
+
expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
|
93
92
|
```
|
94
93
|
|
95
94
|
#### Using scale factor in spec
|
@@ -97,7 +96,7 @@ expect { ... }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
|
|
97
96
|
Let's suppose your action accepts parameter, which can make impact on the number of returned records:
|
98
97
|
|
99
98
|
```ruby
|
100
|
-
get :index, params: {
|
99
|
+
get :index, params: {per_page: 10}
|
101
100
|
```
|
102
101
|
|
103
102
|
Then it is enough to just change `per_page` parameter between executions and do not recreate records in DB. For this purpose, you can use `current_scale` method in your example:
|
@@ -107,7 +106,36 @@ context "N+1", :n_plus_one do
|
|
107
106
|
before { create_list :post, 3 }
|
108
107
|
|
109
108
|
specify do
|
110
|
-
expect { get :index, params: {
|
109
|
+
expect { get :index, params: {per_page: current_scale} }.to perform_constant_number_of_queries
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
### Expectations in execution block
|
115
|
+
|
116
|
+
Both rspec matchers allows you to put additional expectations inside execution block to ensure that tested piece of code actually does what expected.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
context "N+1", :n_plus_one do
|
120
|
+
specify do
|
121
|
+
expect do
|
122
|
+
expect(my_query).to eq(actuall_results)
|
123
|
+
end.to perform_constant_number_of_queries
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
#### Other available matchers
|
129
|
+
|
130
|
+
`perform_linear_number_of_queries(slope: 1)` allows you to test that a query generates linear number of queries with the given slope.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
context "when has linear query", :n_plus_one do
|
134
|
+
populate { |n| create_list(:post, n) }
|
135
|
+
|
136
|
+
specify do
|
137
|
+
expect { Post.find_each { |p| p.user.name } }
|
138
|
+
.to perform_linear_number_of_queries(slope: 1)
|
111
139
|
end
|
112
140
|
end
|
113
141
|
```
|
@@ -118,8 +146,6 @@ First, add NPlusOneControl to your `test_helper.rb`:
|
|
118
146
|
|
119
147
|
```ruby
|
120
148
|
# test_helper.rb
|
121
|
-
...
|
122
|
-
|
123
149
|
require "n_plus_one_control/minitest"
|
124
150
|
```
|
125
151
|
|
@@ -135,6 +161,18 @@ def test_no_n_plus_one_error
|
|
135
161
|
end
|
136
162
|
```
|
137
163
|
|
164
|
+
You can also use `assert_perform_linear_number_of_queries` to test for linear queries:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
def test_no_n_plus_one_error
|
168
|
+
populate = ->(n) { create_list(:post, n) }
|
169
|
+
|
170
|
+
assert_perform_linear_number_of_queries(slope: 1, populate: populate) do
|
171
|
+
Post.find_each { |p| p.user.name }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
138
176
|
You can also specify custom scale factors or filter patterns:
|
139
177
|
|
140
178
|
```ruby
|
@@ -153,6 +191,12 @@ assert_perform_constant_number_of_queries(
|
|
153
191
|
end
|
154
192
|
```
|
155
193
|
|
194
|
+
It's possible to specify a filter via `NPLUSONE_FILTER` env var, e.g.:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
NPLUSONE_FILTER = users bundle exec rake test
|
198
|
+
```
|
199
|
+
|
156
200
|
You can also specify `populate` as a test class instance method:
|
157
201
|
|
158
202
|
```ruby
|
@@ -165,6 +209,7 @@ def test_no_n_plus_one_error
|
|
165
209
|
get :index
|
166
210
|
end
|
167
211
|
end
|
212
|
+
|
168
213
|
```
|
169
214
|
|
170
215
|
As in RSpec, you can use `current_scale` factor instead of `populate` block:
|
@@ -172,7 +217,7 @@ As in RSpec, you can use `current_scale` factor instead of `populate` block:
|
|
172
217
|
```ruby
|
173
218
|
def test_no_n_plus_one_error
|
174
219
|
assert_perform_constant_number_of_queries do
|
175
|
-
get :index, params: {
|
220
|
+
get :index, params: {per_page: current_scale}
|
176
221
|
end
|
177
222
|
end
|
178
223
|
```
|
@@ -223,6 +268,7 @@ end
|
|
223
268
|
```
|
224
269
|
|
225
270
|
If your `warmup` and testing procs are identical, you can use:
|
271
|
+
|
226
272
|
```ruby
|
227
273
|
expext { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only
|
228
274
|
```
|
@@ -254,7 +300,7 @@ NPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
|
|
254
300
|
# ActiveSupport notifications event to track queries.
|
255
301
|
# We track ActiveRecord event by default,
|
256
302
|
# but can also track rom-rb events ('sql.rom') as well.
|
257
|
-
NPlusOneControl.event =
|
303
|
+
NPlusOneControl.event = "sql.active_record"
|
258
304
|
|
259
305
|
# configure transactional behavour for populate method
|
260
306
|
# in case of use multiple database connections
|
@@ -268,6 +314,21 @@ NPlusOneControl::Executor.tap do |executor|
|
|
268
314
|
connections.each(&:rollback_transaction)
|
269
315
|
end
|
270
316
|
end
|
317
|
+
|
318
|
+
# Provide a backtrace cleaner callable object used to filter SQL caller location to display in the verbose mode
|
319
|
+
# Set it to nil to disable tracing.
|
320
|
+
#
|
321
|
+
# In Rails apps, we use Rails.backtrace_cleaner by default.
|
322
|
+
NPlusOneControl.backtrace_cleaner = ->(locations_array) { do_some_filtering(locations_array) }
|
323
|
+
|
324
|
+
# You can also specify the number of backtrace lines to show.
|
325
|
+
# MOTE: It could be specified via NPLUSONE_BACKTRACE env var
|
326
|
+
NPlusOneControl.backtrace_length = 1
|
327
|
+
|
328
|
+
# Sometime queries could be too large to provide any meaningful insight.
|
329
|
+
# You can configure an output length limit for quries in verbose mode by setting the follwing option
|
330
|
+
# NOTE: It could be specified via NPLUSONE_TRUNCATE env var
|
331
|
+
NPlusOneControl.truncate_query_size = 100
|
271
332
|
```
|
272
333
|
|
273
334
|
## How does it work?
|
@@ -276,22 +337,29 @@ Take a look at our [Executor](https://github.com/palkan/n_plus_one_control/blob/
|
|
276
337
|
|
277
338
|
## What's next?
|
278
339
|
|
340
|
+
- More matchers.
|
341
|
+
|
279
342
|
It may be useful to provide more matchers/assertions, for example:
|
280
343
|
|
281
344
|
```ruby
|
282
345
|
|
283
346
|
# Actually, that means that it is N+1))
|
284
|
-
assert_linear_number_of_queries {
|
347
|
+
assert_linear_number_of_queries { some_code }
|
285
348
|
|
286
349
|
# But we can tune it with `coef` and handle such cases as selecting in batches
|
287
350
|
assert_linear_number_of_queries(coef: 0.1) do
|
288
|
-
Post.find_in_batches {
|
351
|
+
Post.find_in_batches { some_code }
|
289
352
|
end
|
290
353
|
|
291
354
|
# probably, also make sense to add another curve types
|
292
|
-
assert_logarithmic_number_of_queries {
|
355
|
+
assert_logarithmic_number_of_queries { some_code }
|
293
356
|
```
|
294
357
|
|
358
|
+
- Support custom non-SQL events.
|
359
|
+
|
360
|
+
N+1 problem is not a database specific: we can have N+1 Redis calls, N+1 HTTP external requests, etc.
|
361
|
+
We can make `n_plus_one_control` customizable to support these scenarios (technically, we need to make it possible to handle different payload in the event subscriber).
|
362
|
+
|
295
363
|
If you want to discuss or implement any of these, feel free to open an [issue](https://github.com/palkan/n_plus_one_control/issues) or propose a [pull request](https://github.com/palkan/n_plus_one_control/pulls).
|
296
364
|
|
297
365
|
## Development
|
@@ -19,10 +19,28 @@ module NPlusOneControl
|
|
19
19
|
@queries
|
20
20
|
end
|
21
21
|
|
22
|
-
def callback(_name, _start, _finish, _message_id, values)
|
22
|
+
def callback(_name, _start, _finish, _message_id, values) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/LineLength
|
23
23
|
return if %w[CACHE SCHEMA].include? values[:name]
|
24
|
+
return if values[:sql].match?(NPlusOneControl.ignore)
|
24
25
|
|
25
|
-
|
26
|
+
return unless @pattern.nil? || (values[:sql] =~ @pattern)
|
27
|
+
|
28
|
+
query = values[:sql]
|
29
|
+
|
30
|
+
if NPlusOneControl.backtrace_cleaner && NPlusOneControl.verbose
|
31
|
+
source = extract_query_source_location(caller)
|
32
|
+
|
33
|
+
query = "#{query}\n ↳ #{source.join("\n")}" unless source.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
@queries << query
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def extract_query_source_location(locations)
|
42
|
+
NPlusOneControl.backtrace_cleaner.call(locations.lazy)
|
43
|
+
.take(NPlusOneControl.backtrace_length).to_a
|
26
44
|
end
|
27
45
|
end
|
28
46
|
|
@@ -18,7 +18,7 @@ module NPlusOneControl
|
|
18
18
|
|
19
19
|
@executor = NPlusOneControl::Executor.new(
|
20
20
|
population: populate || population_method,
|
21
|
-
matching: matching ||
|
21
|
+
matching: matching || NPlusOneControl.default_matching,
|
22
22
|
scale_factors: scale_factors || NPlusOneControl.default_scale_factors
|
23
23
|
)
|
24
24
|
|
@@ -26,7 +26,30 @@ module NPlusOneControl
|
|
26
26
|
|
27
27
|
counts = queries.map(&:last).map(&:size)
|
28
28
|
|
29
|
-
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)
|
30
53
|
end
|
31
54
|
|
32
55
|
def current_scale
|
@@ -42,6 +65,16 @@ module NPlusOneControl
|
|
42
65
|
def population_method
|
43
66
|
methods.include?(:populate) ? method(:populate) : nil
|
44
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
|
77
|
+
end
|
45
78
|
end
|
46
79
|
end
|
47
80
|
|
@@ -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
|
@@ -9,18 +9,18 @@ module NPlusOneControl
|
|
9
9
|
# Setup warmup block, wich will run before matching
|
10
10
|
# for example, if using cache, then later queries
|
11
11
|
# will perform less DB queries than first
|
12
|
-
def warmup
|
13
|
-
return @warmup unless
|
12
|
+
def warmup(&block)
|
13
|
+
return @warmup unless block
|
14
14
|
|
15
|
-
@warmup =
|
15
|
+
@warmup = block
|
16
16
|
end
|
17
17
|
|
18
18
|
# Setup populate callback, which is used
|
19
19
|
# to prepare data for each run.
|
20
|
-
def populate
|
21
|
-
return @populate unless
|
20
|
+
def populate(&block)
|
21
|
+
return @populate unless block
|
22
22
|
|
23
|
-
@populate =
|
23
|
+
@populate = block
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
data/lib/n_plus_one_control/rspec/{matcher.rb → matchers/perform_constant_number_of_queries.rb}
RENAMED
@@ -16,7 +16,7 @@
|
|
16
16
|
@warmup = true
|
17
17
|
end
|
18
18
|
|
19
|
-
match do |actual, *_args|
|
19
|
+
match(notify_expectation_failures: true) do |actual, *_args|
|
20
20
|
raise ArgumentError, "Block is required" unless actual.is_a? Proc
|
21
21
|
|
22
22
|
raise "Missing tag :n_plus_one" unless
|
@@ -27,8 +27,7 @@
|
|
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,
|
@@ -47,6 +46,6 @@
|
|
47
46
|
raise "This matcher doesn't support negation"
|
48
47
|
end
|
49
48
|
|
50
|
-
failure_message { |_actual| NPlusOneControl.failure_message(@queries) }
|
49
|
+
failure_message { |_actual| NPlusOneControl.failure_message(:constant_queries, @queries) }
|
51
50
|
end
|
52
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(notify_expectation_failures: true) 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
|
@@ -4,7 +4,8 @@ gem "rspec-core", ">= 3.5"
|
|
4
4
|
|
5
5
|
require "n_plus_one_control"
|
6
6
|
require "n_plus_one_control/rspec/dsl"
|
7
|
-
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"
|
8
9
|
require "n_plus_one_control/rspec/context"
|
9
10
|
|
10
11
|
module NPlusOneControl
|
data/lib/n_plus_one_control.rb
CHANGED
@@ -6,7 +6,7 @@ require "n_plus_one_control/executor"
|
|
6
6
|
# RSpec and Minitest matchers to prevent N+1 queries problem.
|
7
7
|
module NPlusOneControl
|
8
8
|
# Used to extract a table name from a query
|
9
|
-
EXTRACT_TABLE_RXP = /(insert into|update|delete from|from) ['"](\S+)['"]/i.freeze
|
9
|
+
EXTRACT_TABLE_RXP = /(insert into|update|delete from|from) ['"`](\S+)['"`]/i.freeze
|
10
10
|
|
11
11
|
# Used to convert a query part extracted by the regexp above to the corresponding
|
12
12
|
# human-friendly type
|
@@ -18,10 +18,18 @@ module NPlusOneControl
|
|
18
18
|
}.freeze
|
19
19
|
|
20
20
|
class << self
|
21
|
-
attr_accessor :default_scale_factors, :verbose, :show_table_stats, :ignore, :event
|
21
|
+
attr_accessor :default_scale_factors, :verbose, :show_table_stats, :ignore, :event,
|
22
|
+
:backtrace_cleaner, :backtrace_length, :truncate_query_size
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
attr_reader :default_matching
|
25
|
+
|
26
|
+
FAILURE_MESSAGES = {
|
27
|
+
constant_queries: "Expected to make the same number of queries",
|
28
|
+
linear_queries: "Expected to make linear number of queries"
|
29
|
+
}
|
30
|
+
|
31
|
+
def failure_message(type, queries) # rubocop:disable Metrics/MethodLength
|
32
|
+
msg = ["#{FAILURE_MESSAGES[type]}, but got:\n"]
|
25
33
|
queries.each do |(scale, data)|
|
26
34
|
msg << " #{data.size} for N=#{scale}\n"
|
27
35
|
end
|
@@ -30,8 +38,8 @@ module NPlusOneControl
|
|
30
38
|
|
31
39
|
if verbose
|
32
40
|
queries.each do |(scale, data)|
|
33
|
-
msg << "
|
34
|
-
msg << data.map { |sql| "
|
41
|
+
msg << "Queries for N=#{scale}\n"
|
42
|
+
msg << data.map { |sql| " #{truncate_query(sql)}\n" }.join.to_s
|
35
43
|
end
|
36
44
|
end
|
37
45
|
|
@@ -39,7 +47,7 @@ module NPlusOneControl
|
|
39
47
|
end
|
40
48
|
|
41
49
|
def table_usage_stats(runs) # rubocop:disable Metrics/MethodLength
|
42
|
-
msg = ["
|
50
|
+
msg = ["Unmatched query numbers by tables:\n"]
|
43
51
|
|
44
52
|
before, after = runs.map do |queries|
|
45
53
|
queries.group_by do |query|
|
@@ -58,6 +66,38 @@ module NPlusOneControl
|
|
58
66
|
|
59
67
|
msg
|
60
68
|
end
|
69
|
+
|
70
|
+
def default_matching=(val)
|
71
|
+
unless val
|
72
|
+
@default_matching = nil
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
@default_matching =
|
77
|
+
if val.is_a?(Regexp)
|
78
|
+
val
|
79
|
+
else
|
80
|
+
Regexp.new(val, Regexp::MULTILINE | Regexp::IGNORECASE)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def truncate_query(sql)
|
87
|
+
return sql unless truncate_query_size
|
88
|
+
|
89
|
+
# Only truncate query, leave tracing (if any) as is
|
90
|
+
parts = sql.split(/(\s+↳)/)
|
91
|
+
|
92
|
+
parts[0] =
|
93
|
+
if truncate_query_size < 4
|
94
|
+
"..."
|
95
|
+
else
|
96
|
+
parts[0][0..(truncate_query_size - 4)] + "..."
|
97
|
+
end
|
98
|
+
|
99
|
+
parts.join
|
100
|
+
end
|
61
101
|
end
|
62
102
|
|
63
103
|
# Scale factors to use.
|
@@ -65,7 +105,7 @@ module NPlusOneControl
|
|
65
105
|
self.default_scale_factors = [2, 3]
|
66
106
|
|
67
107
|
# Print performed queries if true
|
68
|
-
self.verbose = ENV[
|
108
|
+
self.verbose = ENV["NPLUSONE_VERBOSE"] == "1"
|
69
109
|
|
70
110
|
# Print table hits difference
|
71
111
|
self.show_table_stats = true
|
@@ -76,5 +116,16 @@ module NPlusOneControl
|
|
76
116
|
# ActiveSupport notifications event to track queries.
|
77
117
|
# We track ActiveRecord event by default,
|
78
118
|
# but can also track rom-rb events ('sql.rom') as well.
|
79
|
-
self.event =
|
119
|
+
self.event = "sql.active_record"
|
120
|
+
|
121
|
+
# Default query filtering applied if none provided explicitly
|
122
|
+
self.default_matching = ENV["NPLUSONE_FILTER"] || /^SELECT/i
|
123
|
+
|
124
|
+
# Truncate queries in verbose mode to fit the length
|
125
|
+
self.truncate_query_size = ENV["NPLUSONE_TRUNCATE"]&.to_i
|
126
|
+
|
127
|
+
# Define the number of backtrace lines to show
|
128
|
+
self.backtrace_length = ENV.fetch("NPLUSONE_BACKTRACE", 1).to_i
|
80
129
|
end
|
130
|
+
|
131
|
+
require "n_plus_one_control/railtie" if defined?(Rails::Railtie)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.2
|
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: 2021-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -28,16 +28,16 @@ dependencies:
|
|
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
|
@@ -152,10 +96,12 @@ files:
|
|
152
96
|
- lib/n_plus_one_control.rb
|
153
97
|
- lib/n_plus_one_control/executor.rb
|
154
98
|
- lib/n_plus_one_control/minitest.rb
|
99
|
+
- lib/n_plus_one_control/railtie.rb
|
155
100
|
- lib/n_plus_one_control/rspec.rb
|
156
101
|
- lib/n_plus_one_control/rspec/context.rb
|
157
102
|
- lib/n_plus_one_control/rspec/dsl.rb
|
158
|
-
- 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
|
159
105
|
- lib/n_plus_one_control/version.rb
|
160
106
|
homepage: http://github.com/palkan/n_plus_one_control
|
161
107
|
licenses:
|
@@ -166,7 +112,7 @@ metadata:
|
|
166
112
|
documentation_uri: http://github.com/palkan/n_plus_one_control
|
167
113
|
homepage_uri: http://github.com/palkan/n_plus_one_control
|
168
114
|
source_code_uri: http://github.com/palkan/n_plus_one_control
|
169
|
-
post_install_message:
|
115
|
+
post_install_message:
|
170
116
|
rdoc_options: []
|
171
117
|
require_paths:
|
172
118
|
- lib
|
@@ -174,15 +120,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
174
120
|
requirements:
|
175
121
|
- - ">="
|
176
122
|
- !ruby/object:Gem::Version
|
177
|
-
version: 2.
|
123
|
+
version: 2.5.0
|
178
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
125
|
requirements:
|
180
126
|
- - ">="
|
181
127
|
- !ruby/object:Gem::Version
|
182
128
|
version: '0'
|
183
129
|
requirements: []
|
184
|
-
rubygems_version: 3.0.
|
185
|
-
signing_key:
|
130
|
+
rubygems_version: 3.0.3
|
131
|
+
signing_key:
|
186
132
|
specification_version: 4
|
187
133
|
summary: RSpec and Minitest matchers to prevent N+1 queries problem
|
188
134
|
test_files: []
|