n_plus_one_control 0.1.3 → 0.4.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 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