n_plus_one_control 0.1.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f39c7dee63a881210d82f9459af15c81969f6346
4
- data.tar.gz: 1a303ce2a4b52354cf3b4036c1f925c017308097
2
+ SHA256:
3
+ metadata.gz: b20b8f4269aac76f3da642b271b93ddee2adf508539bba6852e02225695de155
4
+ data.tar.gz: e739f342b4d46cc451229f3a065cb0e2b83ee28c301d99045d0f094e74bc137b
5
5
  SHA512:
6
- metadata.gz: 4bd630b55700e20ba3cb00523d7e2b023819ffdfd802a1daae90e7532dfa86b8c4ac170378fde819b5810c694268df07d08b361c0fae84bbcbbfeff198a3e5fc
7
- data.tar.gz: 6bc7a3c38fda5837c6f8a3e0cabc3a118ef0c95bb3ffc1066fae066fe9a86a7c14ac06876704326520594b91230b5e78c1015e00f6160ee1e5e3c58953e46396
6
+ metadata.gz: 74805b4ea497ff96bb882557ed8a14e04d32cf5ea5c62e9c65167647c880275aec9d0f8a405a298554e9cfecbb20049d99f630b977475a75122d847c7b27e1a9
7
+ data.tar.gz: 19063ec1fb2a84edcfd0e38075a3858e239a438803ffb40e03916bc8eb5d49dda9e8b03a30a60788e4e00f9c5484811740448fe3241e64658a78ccf884619321
@@ -0,0 +1,14 @@
1
+ ## master (unreleased)
2
+
3
+ ## 0.4.1 (2020-09-04)
4
+
5
+ - Enhance failure message by showing differences in table hits. ([@palkan][])
6
+
7
+ ## 0.4.0 (2020-07-20)
8
+
9
+ - Make scale factor available in tests via `#current_scale` method. ([@Earendil95][])
10
+
11
+ - Start keeping a changelog. ([@palkan][])
12
+
13
+ [@Earendil95]: https://github.com/Earendil95
14
+ [@palkan]: https://github.com/palkan
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017 palkan
3
+ Copyright (c) 2017-2020 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -92,6 +92,26 @@ expect { ... }.to perform_constant_number_of_queries.matching(/INSERT/)
92
92
  expect { ... }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
93
93
  ```
94
94
 
95
+ #### Using scale factor in spec
96
+
97
+ Let's suppose your action accepts parameter, which can make impact on the number of returned records:
98
+
99
+ ```ruby
100
+ get :index, params: { per_page: 10 }
101
+ ```
102
+
103
+ 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:
104
+
105
+ ```ruby
106
+ context "N+1", :n_plus_one do
107
+ before { create_list :post, 3 }
108
+
109
+ specify do
110
+ expect { get :index, params: { per_page: current_scale } }.to perform_constant_number_of_queries
111
+ end
112
+ end
113
+ ```
114
+
95
115
  ### Minitest
96
116
 
97
117
  First, add NPlusOneControl to your `test_helper.rb`:
@@ -147,6 +167,66 @@ def test_no_n_plus_one_error
147
167
  end
148
168
  ```
149
169
 
170
+ As in RSpec, you can use `current_scale` factor instead of `populate` block:
171
+
172
+ ```ruby
173
+ def test_no_n_plus_one_error
174
+ assert_perform_constant_number_of_queries do
175
+ get :index, params: { per_page: current_scale }
176
+ end
177
+ end
178
+ ```
179
+
180
+ ### With caching
181
+
182
+ If you use caching you can face the problem when first request performs more DB queries than others. The solution is:
183
+
184
+ ```ruby
185
+ # RSpec
186
+
187
+ context "N + 1", :n_plus_one do
188
+ populate { |n| create_list :post, n }
189
+
190
+ warmup { get :index } # cache something must be cached
191
+
192
+ specify do
193
+ expect { get :index }.to perform_constant_number_of_queries
194
+ end
195
+ end
196
+
197
+ # Minitest
198
+
199
+ def populate(n)
200
+ create_list(:post, n)
201
+ end
202
+
203
+ def warmup
204
+ get :index
205
+ end
206
+
207
+ def test_no_n_plus_one_error
208
+ assert_perform_constant_number_of_queries do
209
+ get :index
210
+ end
211
+ end
212
+
213
+ # or with params
214
+
215
+ def test_no_n_plus_one
216
+ populate = ->(n) { create_list(:post, n) }
217
+ warmup = -> { get :index }
218
+
219
+ assert_perform_constant_number_of_queries population: populate, warmup: warmup do
220
+ get :index
221
+ end
222
+ end
223
+ ```
224
+
225
+ If your `warmup` and testing procs are identical, you can use:
226
+ ```ruby
227
+ expext { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only
228
+ ```
229
+
150
230
  ### Configuration
151
231
 
152
232
  There are some global configuration parameters (and their corresponding defaults):
@@ -160,6 +240,14 @@ NPlusOneControl.default_scale_factors = [2, 3]
160
240
  # You can activate verbosity through env variable NPLUSONE_VERBOSE=1
161
241
  NPlusOneControl.verbose = false
162
242
 
243
+ # Print table hits difference, for example:
244
+ #
245
+ # Unmatched query numbers by tables:
246
+ # users (SELECT): 2 != 3
247
+ # events (INSERT): 1 != 2
248
+ #
249
+ self.show_table_stats = true
250
+
163
251
  # Ignore matching queries
164
252
  NPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
165
253
 
@@ -167,6 +255,19 @@ NPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
167
255
  # We track ActiveRecord event by default,
168
256
  # but can also track rom-rb events ('sql.rom') as well.
169
257
  NPlusOneControl.event = 'sql.active_record'
258
+
259
+ # configure transactional behavour for populate method
260
+ # in case of use multiple database connections
261
+ NPlusOneControl::Executor.tap do |executor|
262
+ connections = ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
263
+
264
+ executor.transaction_begin = -> do
265
+ connections.each { |connection| connection.begin_transaction(joinable: false) }
266
+ end
267
+ executor.transaction_rollback = -> do
268
+ connections.each(&:rollback_transaction)
269
+ end
270
+ end
170
271
  ```
171
272
 
172
273
  ## How does it work?
@@ -180,7 +281,7 @@ It may be useful to provide more matchers/assertions, for example:
180
281
  ```ruby
181
282
 
182
283
  # Actually, that means that it is N+1))
183
- assert_linear_number_of_queries { ... }
284
+ assert_linear_number_of_queries { ... }
184
285
 
185
286
  # But we can tune it with `coef` and handle such cases as selecting in batches
186
287
  assert_linear_number_of_queries(coef: 0.1) do
@@ -211,4 +312,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/palkan
211
312
  ## License
212
313
 
213
314
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
214
-
@@ -5,17 +5,59 @@ require "n_plus_one_control/executor"
5
5
 
6
6
  # RSpec and Minitest matchers to prevent N+1 queries problem.
7
7
  module NPlusOneControl
8
+ # Used to extract a table name from a query
9
+ EXTRACT_TABLE_RXP = /(insert into|update|delete from|from) ['"](\S+)['"]/i.freeze
10
+
11
+ # Used to convert a query part extracted by the regexp above to the corresponding
12
+ # human-friendly type
13
+ QUERY_PART_TO_TYPE = {
14
+ "insert into" => "INSERT",
15
+ "update" => "UPDATE",
16
+ "delete from" => "DELETE",
17
+ "from" => "SELECT"
18
+ }.freeze
19
+
8
20
  class << self
9
- attr_accessor :default_scale_factors, :verbose, :ignore, :event
21
+ attr_accessor :default_scale_factors, :verbose, :show_table_stats, :ignore, :event
10
22
 
11
- def failure_message(queries)
23
+ def failure_message(queries) # rubocop:disable Metrics/MethodLength
12
24
  msg = ["Expected to make the same number of queries, but got:\n"]
13
25
  queries.each do |(scale, data)|
14
26
  msg << " #{data.size} for N=#{scale}\n"
15
- msg << data.map { |sql| " #{sql}\n" }.join.to_s if verbose
16
27
  end
28
+
29
+ msg.concat(table_usage_stats(queries.map(&:last))) if show_table_stats
30
+
31
+ if verbose
32
+ queries.each do |(scale, data)|
33
+ msg << " Queries for N=#{scale}\n"
34
+ msg << data.map { |sql| " #{sql}\n" }.join.to_s
35
+ end
36
+ end
37
+
17
38
  msg.join
18
39
  end
40
+
41
+ def table_usage_stats(runs) # rubocop:disable Metrics/MethodLength
42
+ msg = ["\nUnmatched query numbers by tables:\n"]
43
+
44
+ before, after = runs.map do |queries|
45
+ queries.group_by do |query|
46
+ matches = query.match(EXTRACT_TABLE_RXP)
47
+ next unless matches
48
+
49
+ " #{matches[2]} (#{QUERY_PART_TO_TYPE[matches[1].downcase]})"
50
+ end.transform_values(&:count)
51
+ end
52
+
53
+ before.keys.each do |k|
54
+ next if before[k] == after[k]
55
+
56
+ msg << "#{k}: #{before[k]} != #{after[k]}\n"
57
+ end
58
+
59
+ msg
60
+ end
19
61
  end
20
62
 
21
63
  # Scale factors to use.
@@ -25,6 +67,9 @@ module NPlusOneControl
25
67
  # Print performed queries if true
26
68
  self.verbose = ENV['NPLUSONE_VERBOSE'] == '1'
27
69
 
70
+ # Print table hits difference
71
+ self.show_table_stats = true
72
+
28
73
  # Ignore matching queries
29
74
  self.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
30
75
 
@@ -3,7 +3,7 @@
3
3
  module NPlusOneControl
4
4
  # Runs code for every scale factor
5
5
  # and returns collected queries.
6
- module Executor
6
+ class Executor
7
7
  # Subscribes to ActiveSupport notifications and collect matching queries.
8
8
  class Collector
9
9
  def initialize(pattern)
@@ -21,35 +21,67 @@ module NPlusOneControl
21
21
 
22
22
  def callback(_name, _start, _finish, _message_id, values)
23
23
  return if %w[CACHE SCHEMA].include? values[:name]
24
+
24
25
  @queries << values[:sql] if @pattern.nil? || (values[:sql] =~ @pattern)
25
26
  end
26
27
  end
27
28
 
28
29
  class << self
29
- def call(population:, scale_factors: nil, matching: nil)
30
- raise ArgumentError, "Block is required!" unless block_given?
30
+ attr_accessor :transaction_begin
31
+ attr_accessor :transaction_rollback
32
+ end
33
+
34
+ attr_reader :current_scale
35
+
36
+ self.transaction_begin = -> do
37
+ ActiveRecord::Base.connection.begin_transaction(joinable: false)
38
+ end
39
+
40
+ self.transaction_rollback = -> do
41
+ ActiveRecord::Base.connection.rollback_transaction
42
+ end
43
+
44
+ def initialize(population: nil, scale_factors: nil, matching: nil)
45
+ @population = population
46
+ @scale_factors = scale_factors
47
+ @matching = matching
48
+ end
31
49
 
32
- results = []
33
- collector = Collector.new(matching)
50
+ # rubocop:disable Metrics/MethodLength
51
+ def call
52
+ raise ArgumentError, "Block is required!" unless block_given?
34
53
 
35
- (scale_factors || NPlusOneControl.default_scale_factors).each do |scale|
36
- with_transaction do
37
- population.call(scale)
38
- results << [scale, collector.call { yield }]
39
- end
54
+ results = []
55
+ collector = Collector.new(matching)
56
+
57
+ (scale_factors || NPlusOneControl.default_scale_factors).each do |scale|
58
+ @current_scale = scale
59
+ with_transaction do
60
+ population&.call(scale)
61
+ results << [scale, collector.call { yield }]
40
62
  end
41
- results
42
63
  end
64
+ results
65
+ end
66
+ # rubocop:enable Metrics/MethodLength
43
67
 
44
- private
68
+ private
45
69
 
46
- def with_transaction
47
- return yield unless defined?(ActiveRecord)
48
- ActiveRecord::Base.connection.begin_transaction(joinable: false)
49
- yield
50
- ensure
51
- ActiveRecord::Base.connection.rollback_transaction
52
- end
70
+ def with_transaction
71
+ transaction_begin.call
72
+ yield
73
+ ensure
74
+ transaction_rollback.call
75
+ end
76
+
77
+ def transaction_begin
78
+ self.class.transaction_begin
79
+ end
80
+
81
+ def transaction_rollback
82
+ self.class.transaction_rollback
53
83
  end
84
+
85
+ attr_reader :population, :scale_factors, :matching
54
86
  end
55
87
  end
@@ -8,21 +8,40 @@ module NPlusOneControl
8
8
  def assert_perform_constant_number_of_queries(
9
9
  populate: nil,
10
10
  matching: nil,
11
- scale_factors: nil
11
+ scale_factors: nil,
12
+ warmup: nil
12
13
  )
13
14
 
14
15
  raise ArgumentError, "Block is required" unless block_given?
15
16
 
16
- queries = NPlusOneControl::Executor.call(
17
- population: populate || method(:populate),
17
+ warming_up warmup
18
+
19
+ @executor = NPlusOneControl::Executor.new(
20
+ population: populate || population_method,
18
21
  matching: matching || /^SELECT/i,
19
22
  scale_factors: scale_factors || NPlusOneControl.default_scale_factors
20
- ) { yield }
23
+ )
24
+
25
+ queries = @executor.call { yield }
21
26
 
22
27
  counts = queries.map(&:last).map(&:size)
23
28
 
24
29
  assert counts.max == counts.min, NPlusOneControl.failure_message(queries)
25
30
  end
31
+
32
+ def current_scale
33
+ @executor&.current_scale
34
+ end
35
+
36
+ private
37
+
38
+ def warming_up(warmup)
39
+ (warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
40
+ end
41
+
42
+ def population_method
43
+ methods.include?(:populate) ? method(:populate) : nil
44
+ end
26
45
  end
27
46
  end
28
47
 
@@ -1,5 +1,7 @@
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
7
  require "n_plus_one_control/rspec/matcher"
@@ -11,5 +13,6 @@ module NPlusOneControl
11
13
  end
12
14
 
13
15
  ::RSpec.configure do |config|
14
- config.extend NPlusOneControl::RSpec::DSL, n_plus_one: true
16
+ config.extend NPlusOneControl::RSpec::DSL::ClassMethods, n_plus_one: true
17
+ config.include NPlusOneControl::RSpec::DSL, n_plus_one: true
15
18
  end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ::RSpec.shared_context "n_plus_one_control", n_plus_one: true do
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
10
+
11
+ let(:n_plus_one_warmup) do |ex|
12
+ return if ex.example_group.warmup.nil?
13
+
14
+ -> { ex.instance_exec(&ex.example_group.warmup) }
15
+ end
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.include_context "n_plus_one_control", n_plus_one: true
19
20
  end
@@ -2,14 +2,32 @@
2
2
 
3
3
  module NPlusOneControl
4
4
  module RSpec
5
- # Extends RSpec ExampleGroup with populate method
5
+ # Includes scale method into RSpec Example
6
6
  module DSL
7
- # Setup populate callback, which is used
8
- # to prepare data for each run.
9
- def populate
10
- return @populate unless block_given?
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?
11
14
 
12
- @populate = Proc.new
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
25
+ end
26
+
27
+ attr_accessor :executor
28
+
29
+ def current_scale
30
+ executor&.current_scale
13
31
  end
14
32
  end
15
33
  end
@@ -12,6 +12,10 @@
12
12
  @pattern = pattern
13
13
  end
14
14
 
15
+ chain :with_warming_up do
16
+ @warmup = true
17
+ end
18
+
15
19
  match do |actual, *_args|
16
20
  raise ArgumentError, "Block is required" unless actual.is_a? Proc
17
21
 
@@ -19,17 +23,21 @@
19
23
  @matcher_execution_context.respond_to?(:n_plus_one_populate)
20
24
 
21
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?
22
29
 
23
30
  # by default we're looking for select queries
24
31
  pattern = @pattern || /^SELECT/i
25
32
 
26
- @queries = NPlusOneControl::Executor.call(
33
+ @matcher_execution_context.executor = NPlusOneControl::Executor.new(
27
34
  population: populate,
28
35
  matching: pattern,
29
- scale_factors: @factors,
30
- &actual
36
+ scale_factors: @factors
31
37
  )
32
38
 
39
+ @queries = @matcher_execution_context.executor.call(&actual)
40
+
33
41
  counts = @queries.map(&:last).map(&:size)
34
42
 
35
43
  counts.max == counts.min
@@ -41,3 +49,4 @@
41
49
 
42
50
  failure_message { |_actual| NPlusOneControl.failure_message(@queries) }
43
51
  end
52
+ # rubocop:enable Metrics/BlockLength
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NPlusOneControl
4
- VERSION = "0.1.3"
4
+ VERSION = "0.4.1"
5
5
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n_plus_one_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-27 00:00:00.000000000 Z
11
+ date: 2020-09-04 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
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.49'
89
+ version: 0.61.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.49'
96
+ version: 0.61.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: activerecord
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -112,16 +112,16 @@ dependencies:
112
112
  name: sqlite3
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ">="
115
+ - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: 1.3.6
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ">="
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: 1.3.6
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: pry-byebug
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -139,26 +139,16 @@ dependencies:
139
139
  description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
140
140
  \ Evaluates code under consideration several times with different scale factors\n
141
141
  \ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead
142
- of O(N)).\n\n Example:\n\n ```ruby\n context \"N+1\", :n_plus_one do\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
- \ "
142
+ of O(N)).\n "
146
143
  email:
147
144
  - dementiev.vm@gmail.com
148
145
  executables: []
149
146
  extensions: []
150
147
  extra_rdoc_files: []
151
148
  files:
152
- - ".gitignore"
153
- - ".rspec"
154
- - ".rubocop.yml"
155
- - ".travis.yml"
156
- - Gemfile
149
+ - CHANGELOG.md
157
150
  - LICENSE.txt
158
151
  - README.md
159
- - Rakefile
160
- - bin/console
161
- - bin/setup
162
152
  - lib/n_plus_one_control.rb
163
153
  - lib/n_plus_one_control/executor.rb
164
154
  - lib/n_plus_one_control/minitest.rb
@@ -167,19 +157,15 @@ files:
167
157
  - lib/n_plus_one_control/rspec/dsl.rb
168
158
  - lib/n_plus_one_control/rspec/matcher.rb
169
159
  - 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
160
  homepage: http://github.com/palkan/n_plus_one_control
180
161
  licenses:
181
162
  - MIT
182
- metadata: {}
163
+ metadata:
164
+ bug_tracker_uri: http://github.com/palkan/n_plus_one_control/issues
165
+ changelog_uri: https://github.com/palkan/n_plus_one_control/blob/master/CHANGELOG.md
166
+ documentation_uri: http://github.com/palkan/n_plus_one_control
167
+ homepage_uri: http://github.com/palkan/n_plus_one_control
168
+ source_code_uri: http://github.com/palkan/n_plus_one_control
183
169
  post_install_message:
184
170
  rdoc_options: []
185
171
  require_paths:
@@ -195,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
181
  - !ruby/object:Gem::Version
196
182
  version: '0'
197
183
  requirements: []
198
- rubyforge_project:
199
- rubygems_version: 2.6.13
184
+ rubygems_version: 3.0.6
200
185
  signing_key:
201
186
  specification_version: 4
202
187
  summary: RSpec and Minitest matchers to prevent N+1 queries problem
data/.gitignore DELETED
@@ -1,11 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- /log/
11
- *.gem
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --format documentation
2
- --color
@@ -1,71 +0,0 @@
1
- AllCops:
2
- Include:
3
- - 'lib/**/*.rb'
4
- - 'lib/**/*.rake'
5
- - 'spec/**/*.rb'
6
- Exclude:
7
- - 'bin/**/*'
8
- - 'spec/dummy/**/*'
9
- - 'tmp/**/*'
10
- - 'Rakefile'
11
- - 'Gemfile'
12
- - '*.gemspec'
13
- DisplayCopNames: true
14
- StyleGuideCopsOnly: false
15
- TargetRubyVersion: 2.4
16
-
17
- Rails:
18
- Enabled: false
19
-
20
- Style/AccessorMethodName:
21
- Enabled: false
22
-
23
- Style/TrivialAccessors:
24
- Enabled: false
25
-
26
- Style/Documentation:
27
- Exclude:
28
- - 'spec/**/*.rb'
29
- - 'tests/**/*.rb'
30
-
31
- Style/StringLiterals:
32
- Enabled: false
33
-
34
- Style/RegexpLiteral:
35
- Enabled: false
36
-
37
- Style/SpaceInsideStringInterpolation:
38
- EnforcedStyle: no_space
39
-
40
- Style/ClassAndModuleChildren:
41
- Enabled: false
42
-
43
- Style/BlockDelimiters:
44
- Exclude:
45
- - 'spec/**/*.rb'
46
-
47
- Lint/AmbiguousRegexpLiteral:
48
- Enabled: false
49
-
50
-
51
- Metrics/MethodLength:
52
- Exclude:
53
- - 'spec/**/*.rb'
54
-
55
- Metrics/AbcSize:
56
- Max: 20
57
-
58
- Metrics/LineLength:
59
- Max: 100
60
- Exclude:
61
- - 'spec/**/*.rb'
62
-
63
- Metrics/BlockLength:
64
- Exclude:
65
- - 'spec/**/*.rb'
66
-
67
- Rails/Date:
68
- Enabled: false
69
-
70
- Rails/TimeZone:
71
- Enabled: false
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.3.3
5
- before_install: gem install bundler -v 1.13.6
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in n_plus_one_control.gemspec
4
- gemspec
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]
@@ -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
@@ -1,8 +0,0 @@
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
@@ -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
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe NPlusOneControl do
6
- it "has a version number" do
7
- expect(NPlusOneControl::VERSION).not_to be nil
8
- end
9
- end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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