bullet 5.9.0 → 6.1.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +27 -0
  4. data/Gemfile.rails-4.0 +1 -1
  5. data/Gemfile.rails-4.1 +1 -1
  6. data/Gemfile.rails-4.2 +1 -1
  7. data/Gemfile.rails-5.0 +1 -1
  8. data/Gemfile.rails-5.1 +1 -1
  9. data/Gemfile.rails-5.2 +1 -1
  10. data/Gemfile.rails-6.0 +15 -0
  11. data/Gemfile.rails-6.1 +15 -0
  12. data/README.md +31 -10
  13. data/lib/bullet.rb +50 -26
  14. data/lib/bullet/active_job.rb +13 -0
  15. data/lib/bullet/active_record4.rb +9 -32
  16. data/lib/bullet/active_record41.rb +7 -27
  17. data/lib/bullet/active_record42.rb +8 -24
  18. data/lib/bullet/active_record5.rb +188 -179
  19. data/lib/bullet/active_record52.rb +176 -168
  20. data/lib/bullet/active_record60.rb +267 -0
  21. data/lib/bullet/active_record61.rb +267 -0
  22. data/lib/bullet/bullet_xhr.js +63 -0
  23. data/lib/bullet/dependency.rb +50 -36
  24. data/lib/bullet/detector/association.rb +26 -20
  25. data/lib/bullet/detector/base.rb +1 -2
  26. data/lib/bullet/detector/counter_cache.rb +13 -9
  27. data/lib/bullet/detector/n_plus_one_query.rb +22 -12
  28. data/lib/bullet/detector/unused_eager_loading.rb +6 -3
  29. data/lib/bullet/ext/object.rb +4 -2
  30. data/lib/bullet/mongoid4x.rb +2 -6
  31. data/lib/bullet/mongoid5x.rb +2 -6
  32. data/lib/bullet/mongoid6x.rb +2 -6
  33. data/lib/bullet/mongoid7x.rb +2 -6
  34. data/lib/bullet/notification/base.rb +14 -18
  35. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  36. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  37. data/lib/bullet/rack.rb +50 -25
  38. data/lib/bullet/stack_trace_filter.rb +6 -12
  39. data/lib/bullet/version.rb +1 -1
  40. data/lib/generators/bullet/install_generator.rb +23 -23
  41. data/perf/benchmark.rb +8 -14
  42. data/spec/bullet/detector/counter_cache_spec.rb +6 -6
  43. data/spec/bullet/detector/n_plus_one_query_spec.rb +7 -3
  44. data/spec/bullet/detector/unused_eager_loading_spec.rb +19 -6
  45. data/spec/bullet/ext/object_spec.rb +9 -4
  46. data/spec/bullet/notification/base_spec.rb +1 -3
  47. data/spec/bullet/notification/n_plus_one_query_spec.rb +16 -3
  48. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  49. data/spec/bullet/rack_spec.rb +86 -6
  50. data/spec/bullet/registry/association_spec.rb +2 -2
  51. data/spec/bullet/registry/base_spec.rb +1 -1
  52. data/spec/bullet_spec.rb +11 -30
  53. data/spec/integration/active_record/association_spec.rb +44 -136
  54. data/spec/integration/counter_cache_spec.rb +11 -31
  55. data/spec/integration/mongoid/association_spec.rb +18 -32
  56. data/spec/models/folder.rb +1 -2
  57. data/spec/models/group.rb +1 -2
  58. data/spec/models/page.rb +1 -2
  59. data/spec/models/writer.rb +1 -2
  60. data/spec/spec_helper.rb +6 -10
  61. data/spec/support/bullet_ext.rb +8 -9
  62. data/spec/support/mongo_seed.rb +2 -16
  63. data/test.sh +1 -0
  64. metadata +12 -7
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module SaveWithBulletSupport
5
+ def _create_record(*)
6
+ super do
7
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(self)
8
+ yield(self) if block_given?
9
+ end
10
+ end
11
+ end
12
+
13
+ module ActiveRecord
14
+ def self.enable
15
+ require 'active_record'
16
+ ::ActiveRecord::Base.extend(
17
+ Module.new do
18
+ def find_by_sql(sql, binds = [], preparable: nil, &block)
19
+ result = super
20
+ if Bullet.start?
21
+ if result.is_a? Array
22
+ if result.size > 1
23
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
24
+ Bullet::Detector::CounterCache.add_possible_objects(result)
25
+ elsif result.size == 1
26
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
27
+ Bullet::Detector::CounterCache.add_impossible_object(result.first)
28
+ end
29
+ elsif result.is_a? ::ActiveRecord::Base
30
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result)
31
+ Bullet::Detector::CounterCache.add_impossible_object(result)
32
+ end
33
+ end
34
+ result
35
+ end
36
+ end
37
+ )
38
+
39
+ ::ActiveRecord::Base.prepend(SaveWithBulletSupport)
40
+
41
+ ::ActiveRecord::Relation.prepend(
42
+ Module.new do
43
+ # if select a collection of objects, then these objects have possible to cause N+1 query.
44
+ # if select only one object, then the only one object has impossible to cause N+1 query.
45
+ def records
46
+ result = super
47
+ if Bullet.start?
48
+ if result.first.class.name !~ /^HABTM_/
49
+ if result.size > 1
50
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(result)
51
+ Bullet::Detector::CounterCache.add_possible_objects(result)
52
+ elsif result.size == 1
53
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first)
54
+ Bullet::Detector::CounterCache.add_impossible_object(result.first)
55
+ end
56
+ end
57
+ end
58
+ result
59
+ end
60
+ end
61
+ )
62
+
63
+ ::ActiveRecord::Associations::Preloader.prepend(
64
+ Module.new do
65
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
66
+ if Bullet.start?
67
+ records.compact!
68
+ if records.first.class.name !~ /^HABTM_/
69
+ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) }
70
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association)
71
+ end
72
+ end
73
+ super
74
+ end
75
+
76
+ def preloaders_for_reflection(reflection, records, scope)
77
+ if Bullet.start?
78
+ records.compact!
79
+ if records.first.class.name !~ /^HABTM_/
80
+ records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) }
81
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, reflection.name)
82
+ end
83
+ end
84
+ super
85
+ end
86
+ end
87
+ )
88
+
89
+ ::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(
90
+ Module.new do
91
+ def preloaded_records
92
+ if Bullet.start? && !defined?(@preloaded_records)
93
+ source_preloaders.each do |source_preloader|
94
+ reflection_name = source_preloader.send(:reflection).name
95
+ source_preloader.send(:owners).each do |owner|
96
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name)
97
+ end
98
+ end
99
+ end
100
+ super
101
+ end
102
+ end
103
+ )
104
+
105
+ ::ActiveRecord::FinderMethods.prepend(
106
+ Module.new do
107
+ # add includes in scope
108
+ def find_with_associations
109
+ return super { |r| yield r } if block_given?
110
+
111
+ records = super
112
+ if Bullet.start?
113
+ associations = (eager_load_values + includes_values).uniq
114
+ records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) }
115
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations)
116
+ end
117
+ records
118
+ end
119
+ end
120
+ )
121
+
122
+ ::ActiveRecord::Associations::JoinDependency.prepend(
123
+ Module.new do
124
+ def instantiate(result_set, strict_loading_value, &block)
125
+ @bullet_eager_loadings = {}
126
+ records = super
127
+
128
+ if Bullet.start?
129
+ @bullet_eager_loadings.each do |_klazz, eager_loadings_hash|
130
+ objects = eager_loadings_hash.keys
131
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(
132
+ objects,
133
+ eager_loadings_hash[objects.first].to_a
134
+ )
135
+ end
136
+ end
137
+ records
138
+ end
139
+
140
+ def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
141
+ if Bullet.start?
142
+ unless ar_parent.nil?
143
+ parent.children.each do |node|
144
+ key = aliases.column_alias(node, node.primary_key)
145
+ id = row[key]
146
+ next unless id.nil?
147
+
148
+ associations = node.reflection.name
149
+ Bullet::Detector::Association.add_object_associations(ar_parent, associations)
150
+ Bullet::Detector::NPlusOneQuery.call_association(ar_parent, associations)
151
+ @bullet_eager_loadings[ar_parent.class] ||= {}
152
+ @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new
153
+ @bullet_eager_loadings[ar_parent.class][ar_parent] << associations
154
+ end
155
+ end
156
+ end
157
+
158
+ super
159
+ end
160
+
161
+ # call join associations
162
+ def construct_model(record, node, row, model_cache, id, strict_loading_value)
163
+ result = super
164
+
165
+ if Bullet.start?
166
+ associations = node.reflection.name
167
+ Bullet::Detector::Association.add_object_associations(record, associations)
168
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
169
+ @bullet_eager_loadings[record.class] ||= {}
170
+ @bullet_eager_loadings[record.class][record] ||= Set.new
171
+ @bullet_eager_loadings[record.class][record] << associations
172
+ end
173
+
174
+ result
175
+ end
176
+ end
177
+ )
178
+
179
+ ::ActiveRecord::Associations::CollectionAssociation.prepend(
180
+ Module.new do
181
+ def load_target
182
+ records = super
183
+
184
+ if Bullet.start?
185
+ if is_a? ::ActiveRecord::Associations::ThroughAssociation
186
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
187
+ association = owner.association(reflection.through_reflection.name)
188
+ Array(association.target).each do |through_record|
189
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
190
+ end
191
+
192
+ if reflection.through_reflection != through_reflection
193
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
194
+ end
195
+ end
196
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
197
+ if records.first.class.name !~ /^HABTM_/
198
+ if records.size > 1
199
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
200
+ Bullet::Detector::CounterCache.add_possible_objects(records)
201
+ elsif records.size == 1
202
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
203
+ Bullet::Detector::CounterCache.add_impossible_object(records.first)
204
+ end
205
+ end
206
+ end
207
+ records
208
+ end
209
+
210
+ def empty?
211
+ if Bullet.start? && !reflection.has_cached_counter?
212
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
213
+ end
214
+ super
215
+ end
216
+
217
+ def include?(object)
218
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start?
219
+ super
220
+ end
221
+ end
222
+ )
223
+
224
+ ::ActiveRecord::Associations::SingularAssociation.prepend(
225
+ Module.new do
226
+ # call has_one and belongs_to associations
227
+ def target
228
+ result = super()
229
+
230
+ if Bullet.start?
231
+ if owner.class.name !~ /^HABTM_/ && !@inversed
232
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
233
+
234
+ if Bullet::Detector::NPlusOneQuery.impossible?(owner)
235
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
236
+ else
237
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result
238
+ end
239
+ end
240
+ end
241
+ result
242
+ end
243
+ end
244
+ )
245
+
246
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(
247
+ Module.new do
248
+ def empty?
249
+ result = super
250
+ if Bullet.start? && !reflection.has_cached_counter?
251
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
252
+ end
253
+ result
254
+ end
255
+
256
+ def count_records
257
+ result = reflection.has_cached_counter?
258
+ if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation)
259
+ Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name)
260
+ end
261
+ super
262
+ end
263
+ end
264
+ )
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,63 @@
1
+ (function () {
2
+ var oldOpen = window.XMLHttpRequest.prototype.open;
3
+ var oldSend = window.XMLHttpRequest.prototype.send;
4
+
5
+ /**
6
+ * Return early if we've already extended prototype. This prevents
7
+ * "maximum call stack exceeded" errors when used with Turbolinks.
8
+ * See https://github.com/flyerhzm/bullet/issues/454
9
+ */
10
+ if (isBulletInitiated()) return;
11
+
12
+ function isBulletInitiated() {
13
+ return oldOpen.name == "bulletXHROpen" && oldSend.name == "bulletXHRSend";
14
+ }
15
+ function bulletXHROpen(_, url) {
16
+ this._storedUrl = url;
17
+ return Reflect.apply(oldOpen, this, arguments);
18
+ }
19
+ function bulletXHRSend() {
20
+ if (this.onload) {
21
+ this._storedOnload = this.onload;
22
+ }
23
+ this.addEventListener("load", bulletXHROnload);
24
+ return Reflect.apply(oldSend, this, arguments);
25
+ }
26
+ function bulletXHROnload() {
27
+ if (
28
+ this._storedUrl.startsWith(window.location.protocol + "//" + window.location.host) ||
29
+ !this._storedUrl.startsWith("http") // For relative paths
30
+ ) {
31
+ var bulletFooterText = this.getResponseHeader("X-bullet-footer-text");
32
+ if (bulletFooterText) {
33
+ setTimeout(function() {
34
+ var oldHtml = document.querySelector("#bullet-footer").innerHTML.split("<br>");
35
+ var header = oldHtml[0];
36
+ oldHtml = oldHtml.slice(1, oldHtml.length);
37
+ var newHtml = oldHtml.concat(JSON.parse(bulletFooterText));
38
+ newHtml = newHtml.slice(newHtml.length - 10, newHtml.length); // rotate through 10 most recent
39
+ document.querySelector("#bullet-footer").innerHTML = `${header}<br>${newHtml.join("<br>")}`;
40
+ }, 0);
41
+ }
42
+ var bulletConsoleText = this.getResponseHeader("X-bullet-console-text");
43
+ if (bulletConsoleText && typeof console !== "undefined" && console.log) {
44
+ setTimeout(function() {
45
+ JSON.parse(bulletConsoleText).forEach((message) => {
46
+ if (console.groupCollapsed && console.groupEnd) {
47
+ console.groupCollapsed("Uniform Notifier");
48
+ console.log(message);
49
+ console.groupEnd();
50
+ } else {
51
+ console.log(message);
52
+ }
53
+ });
54
+ }, 0);
55
+ }
56
+ }
57
+ if (this._storedOnload) {
58
+ return Reflect.apply(this._storedOnload, this, arguments);
59
+ }
60
+ }
61
+ window.XMLHttpRequest.prototype.open = bulletXHROpen;
62
+ window.XMLHttpRequest.prototype.send = bulletXHRSend;
63
+ })();
@@ -3,51 +3,53 @@
3
3
  module Bullet
4
4
  module Dependency
5
5
  def mongoid?
6
- @mongoid ||= defined? ::Mongoid
6
+ @mongoid ||= defined?(::Mongoid)
7
7
  end
8
8
 
9
9
  def active_record?
10
- @active_record ||= defined? ::ActiveRecord
11
- end
12
-
13
- def rails?
14
- @rails ||= defined? ::Rails
10
+ @active_record ||= defined?(::ActiveRecord)
15
11
  end
16
12
 
17
13
  def active_record_version
18
- @active_record_version ||= begin
19
- if active_record40?
20
- 'active_record4'
21
- elsif active_record41?
22
- 'active_record41'
23
- elsif active_record42?
24
- 'active_record42'
25
- elsif active_record50?
26
- 'active_record5'
27
- elsif active_record51?
28
- 'active_record5'
29
- elsif active_record52?
30
- 'active_record52'
31
- else
32
- raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet"
33
- end
34
- end
14
+ @active_record_version ||=
15
+ begin
16
+ if active_record40?
17
+ 'active_record4'
18
+ elsif active_record41?
19
+ 'active_record41'
20
+ elsif active_record42?
21
+ 'active_record42'
22
+ elsif active_record50?
23
+ 'active_record5'
24
+ elsif active_record51?
25
+ 'active_record5'
26
+ elsif active_record52?
27
+ 'active_record52'
28
+ elsif active_record60?
29
+ 'active_record60'
30
+ elsif active_record61?
31
+ 'active_record61'
32
+ else
33
+ raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet"
34
+ end
35
+ end
35
36
  end
36
37
 
37
38
  def mongoid_version
38
- @mongoid_version ||= begin
39
- if mongoid4x?
40
- 'mongoid4x'
41
- elsif mongoid5x?
42
- 'mongoid5x'
43
- elsif mongoid6x?
44
- 'mongoid6x'
45
- elsif mongoid7x?
46
- 'mongoid7x'
47
- else
48
- raise "Bullet does not support mongoid #{::Mongoid::VERSION} yet"
49
- end
50
- end
39
+ @mongoid_version ||=
40
+ begin
41
+ if mongoid4x?
42
+ 'mongoid4x'
43
+ elsif mongoid5x?
44
+ 'mongoid5x'
45
+ elsif mongoid6x?
46
+ 'mongoid6x'
47
+ elsif mongoid7x?
48
+ 'mongoid7x'
49
+ else
50
+ raise "Bullet does not support mongoid #{::Mongoid::VERSION} yet"
51
+ end
52
+ end
51
53
  end
52
54
 
53
55
  def active_record4?
@@ -58,6 +60,10 @@ module Bullet
58
60
  active_record? && ::ActiveRecord::VERSION::MAJOR == 5
59
61
  end
60
62
 
63
+ def active_record6?
64
+ active_record? && ::ActiveRecord::VERSION::MAJOR == 6
65
+ end
66
+
61
67
  def active_record40?
62
68
  active_record4? && ::ActiveRecord::VERSION::MINOR == 0
63
69
  end
@@ -82,6 +88,14 @@ module Bullet
82
88
  active_record5? && ::ActiveRecord::VERSION::MINOR == 2
83
89
  end
84
90
 
91
+ def active_record60?
92
+ active_record6? && ::ActiveRecord::VERSION::MINOR == 0
93
+ end
94
+
95
+ def active_record61?
96
+ active_record6? && ::ActiveRecord::VERSION::MINOR == 1
97
+ end
98
+
85
99
  def mongoid4x?
86
100
  mongoid? && ::Mongoid::VERSION =~ /\A4/
87
101
  end
@@ -3,22 +3,28 @@
3
3
  module Bullet
4
4
  module Detector
5
5
  class Association < Base
6
- class <<self
6
+ class << self
7
7
  def add_object_associations(object, associations)
8
8
  return unless Bullet.start?
9
9
  return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable?
10
- return unless object.primary_key_value
10
+ return unless object.bullet_primary_key_value
11
11
 
12
- Bullet.debug('Detector::Association#add_object_associations', "object: #{object.bullet_key}, associations: #{associations}")
12
+ Bullet.debug(
13
+ 'Detector::Association#add_object_associations',
14
+ "object: #{object.bullet_key}, associations: #{associations}"
15
+ )
13
16
  object_associations.add(object.bullet_key, associations)
14
17
  end
15
18
 
16
19
  def add_call_object_associations(object, associations)
17
20
  return unless Bullet.start?
18
21
  return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable?
19
- return unless object.primary_key_value
22
+ return unless object.bullet_primary_key_value
20
23
 
21
- Bullet.debug('Detector::Association#add_call_object_associations', "object: #{object.bullet_key}, associations: #{associations}")
24
+ Bullet.debug(
25
+ 'Detector::Association#add_call_object_associations',
26
+ "object: #{object.bullet_key}, associations: #{associations}"
27
+ )
22
28
  call_object_associations.add(object.bullet_key, associations)
23
29
  end
24
30
 
@@ -40,33 +46,33 @@ module Bullet
40
46
 
41
47
  private
42
48
 
43
- # object_associations keep the object relationships
44
- # that the object has many associations.
45
- # e.g. { "Post:1" => [:comments] }
46
- # the object_associations keep all associations that may be or may no be
47
- # unpreload associations or unused preload associations.
49
+ # object_associations keep the object relationships
50
+ # that the object has many associations.
51
+ # e.g. { "Post:1" => [:comments] }
52
+ # the object_associations keep all associations that may be or may no be
53
+ # unpreload associations or unused preload associations.
48
54
  def object_associations
49
55
  Thread.current[:bullet_object_associations]
50
56
  end
51
57
 
52
- # call_object_associations keep the object relationships
53
- # that object.associations is called.
54
- # e.g. { "Post:1" => [:comments] }
55
- # they are used to detect unused preload associations.
58
+ # call_object_associations keep the object relationships
59
+ # that object.associations is called.
60
+ # e.g. { "Post:1" => [:comments] }
61
+ # they are used to detect unused preload associations.
56
62
  def call_object_associations
57
63
  Thread.current[:bullet_call_object_associations]
58
64
  end
59
65
 
60
- # inversed_objects keeps object relationships
61
- # that association is inversed.
62
- # e.g. { "Comment:1" => ["post"] }
66
+ # inversed_objects keeps object relationships
67
+ # that association is inversed.
68
+ # e.g. { "Comment:1" => ["post"] }
63
69
  def inversed_objects
64
70
  Thread.current[:bullet_inversed_objects]
65
71
  end
66
72
 
67
- # eager_loadings keep the object relationships
68
- # that the associations are preloaded by find :include.
69
- # e.g. { ["Post:1", "Post:2"] => [:comments, :user] }
73
+ # eager_loadings keep the object relationships
74
+ # that the associations are preloaded by find :include.
75
+ # e.g. { ["Post:1", "Post:2"] => [:comments, :user] }
70
76
  def eager_loadings
71
77
  Thread.current[:bullet_eager_loadings]
72
78
  end