bullet 5.7.5 → 6.1.4

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 (76) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +49 -12
  4. data/Gemfile.mongoid-7.0 +15 -0
  5. data/Gemfile.rails-4.0 +1 -1
  6. data/Gemfile.rails-4.1 +1 -1
  7. data/Gemfile.rails-4.2 +1 -1
  8. data/Gemfile.rails-5.0 +1 -1
  9. data/Gemfile.rails-5.1 +1 -1
  10. data/Gemfile.rails-5.2 +2 -2
  11. data/Gemfile.rails-6.0 +15 -0
  12. data/Gemfile.rails-6.1 +15 -0
  13. data/README.md +38 -13
  14. data/Rakefile +1 -1
  15. data/bullet.gemspec +8 -3
  16. data/lib/bullet.rb +50 -22
  17. data/lib/bullet/active_job.rb +13 -0
  18. data/lib/bullet/active_record4.rb +12 -35
  19. data/lib/bullet/active_record41.rb +10 -30
  20. data/lib/bullet/active_record42.rb +12 -27
  21. data/lib/bullet/active_record5.rb +197 -177
  22. data/lib/bullet/active_record52.rb +191 -166
  23. data/lib/bullet/active_record60.rb +278 -0
  24. data/lib/bullet/active_record61.rb +278 -0
  25. data/lib/bullet/bullet_xhr.js +63 -0
  26. data/lib/bullet/dependency.rb +54 -34
  27. data/lib/bullet/detector/association.rb +26 -20
  28. data/lib/bullet/detector/base.rb +1 -2
  29. data/lib/bullet/detector/counter_cache.rb +14 -9
  30. data/lib/bullet/detector/n_plus_one_query.rb +27 -17
  31. data/lib/bullet/detector/unused_eager_loading.rb +7 -3
  32. data/lib/bullet/ext/object.rb +5 -3
  33. data/lib/bullet/ext/string.rb +1 -1
  34. data/lib/bullet/mongoid4x.rb +4 -7
  35. data/lib/bullet/mongoid5x.rb +4 -7
  36. data/lib/bullet/mongoid6x.rb +8 -11
  37. data/lib/bullet/mongoid7x.rb +57 -0
  38. data/lib/bullet/notification/base.rb +15 -19
  39. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  40. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  41. data/lib/bullet/rack.rb +54 -28
  42. data/lib/bullet/stack_trace_filter.rb +39 -30
  43. data/lib/bullet/version.rb +1 -1
  44. data/lib/generators/bullet/install_generator.rb +26 -26
  45. data/perf/benchmark.rb +8 -14
  46. data/spec/bullet/detector/counter_cache_spec.rb +6 -6
  47. data/spec/bullet/detector/n_plus_one_query_spec.rb +30 -3
  48. data/spec/bullet/detector/unused_eager_loading_spec.rb +19 -6
  49. data/spec/bullet/ext/object_spec.rb +9 -4
  50. data/spec/bullet/notification/base_spec.rb +1 -3
  51. data/spec/bullet/notification/n_plus_one_query_spec.rb +16 -3
  52. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  53. data/spec/bullet/rack_spec.rb +140 -5
  54. data/spec/bullet/registry/association_spec.rb +2 -2
  55. data/spec/bullet/registry/base_spec.rb +1 -1
  56. data/spec/bullet_spec.rb +10 -29
  57. data/spec/integration/active_record/association_spec.rb +122 -118
  58. data/spec/integration/counter_cache_spec.rb +11 -31
  59. data/spec/integration/mongoid/association_spec.rb +18 -32
  60. data/spec/models/attachment.rb +5 -0
  61. data/spec/models/client.rb +2 -0
  62. data/spec/models/firm.rb +1 -0
  63. data/spec/models/folder.rb +1 -2
  64. data/spec/models/group.rb +3 -0
  65. data/spec/models/page.rb +1 -2
  66. data/spec/models/post.rb +15 -0
  67. data/spec/models/submission.rb +1 -0
  68. data/spec/models/user.rb +1 -0
  69. data/spec/models/writer.rb +1 -2
  70. data/spec/spec_helper.rb +6 -10
  71. data/spec/support/bullet_ext.rb +8 -9
  72. data/spec/support/mongo_seed.rb +2 -16
  73. data/spec/support/sqlite_seed.rb +17 -2
  74. data/test.sh +2 -0
  75. data/update.sh +1 -0
  76. metadata +24 -11
@@ -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'.freeze, "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'.freeze, "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
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Bullet
4
4
  module Detector
5
- class Base
6
- end
5
+ class Base; end
7
6
  end
8
7
  end
@@ -3,32 +3,37 @@
3
3
  module Bullet
4
4
  module Detector
5
5
  class CounterCache < Base
6
- class <<self
6
+ class << self
7
7
  def add_counter_cache(object, associations)
8
8
  return unless Bullet.start?
9
9
  return unless Bullet.counter_cache_enable?
10
- return unless object.primary_key_value
10
+ return unless object.bullet_primary_key_value
11
11
 
12
- Bullet.debug('Detector::CounterCache#add_counter_cache', "object: #{object.bullet_key}, associations: #{associations}")
13
- if conditions_met?(object, associations)
14
- create_notification object.class.to_s, associations
15
- end
12
+ Bullet.debug(
13
+ 'Detector::CounterCache#add_counter_cache',
14
+ "object: #{object.bullet_key}, associations: #{associations}"
15
+ )
16
+ create_notification object.class.to_s, associations if conditions_met?(object, associations)
16
17
  end
17
18
 
18
19
  def add_possible_objects(object_or_objects)
19
20
  return unless Bullet.start?
20
21
  return unless Bullet.counter_cache_enable?
22
+
21
23
  objects = Array(object_or_objects)
22
- return if objects.map(&:primary_key_value).compact.empty?
24
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
23
25
 
24
- Bullet.debug('Detector::CounterCache#add_possible_objects', "objects: #{objects.map(&:bullet_key).join(', ')}")
26
+ Bullet.debug(
27
+ 'Detector::CounterCache#add_possible_objects',
28
+ "objects: #{objects.map(&:bullet_key).join(', ')}"
29
+ )
25
30
  objects.each { |object| possible_objects.add object.bullet_key }
26
31
  end
27
32
 
28
33
  def add_impossible_object(object)
29
34
  return unless Bullet.start?
30
35
  return unless Bullet.counter_cache_enable?
31
- return unless object.primary_key_value
36
+ return unless object.bullet_primary_key_value
32
37
 
33
38
  Bullet.debug('Detector::CounterCache#add_impossible_object', "object: #{object.bullet_key}")
34
39
  impossible_objects.add object.bullet_key
@@ -6,7 +6,7 @@ module Bullet
6
6
  extend Dependency
7
7
  extend StackTraceFilter
8
8
 
9
- class <<self
9
+ class << self
10
10
  # executed when object.assocations is called.
11
11
  # first, it keeps this method call for object.association.
12
12
  # then, it checks if this associations call is unpreload.
@@ -14,11 +14,15 @@ module Bullet
14
14
  def call_association(object, associations)
15
15
  return unless Bullet.start?
16
16
  return unless Bullet.n_plus_one_query_enable?
17
- return unless object.primary_key_value
17
+ return unless object.bullet_primary_key_value
18
18
  return if inversed_objects.include?(object.bullet_key, associations)
19
+
19
20
  add_call_object_associations(object, associations)
20
21
 
21
- Bullet.debug('Detector::NPlusOneQuery#call_association'.freeze, "object: #{object.bullet_key}, associations: #{associations}")
22
+ Bullet.debug(
23
+ 'Detector::NPlusOneQuery#call_association',
24
+ "object: #{object.bullet_key}, associations: #{associations}"
25
+ )
22
26
  if !excluded_stacktrace_path? && conditions_met?(object, associations)
23
27
  Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
24
28
  create_notification caller_in_project, object.class.to_s, associations
@@ -28,28 +32,35 @@ module Bullet
28
32
  def add_possible_objects(object_or_objects)
29
33
  return unless Bullet.start?
30
34
  return unless Bullet.n_plus_one_query_enable?
35
+
31
36
  objects = Array(object_or_objects)
32
- return if objects.map(&:primary_key_value).compact.empty?
37
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
33
38
 
34
- Bullet.debug('Detector::NPlusOneQuery#add_possible_objects'.freeze, "objects: #{objects.map(&:bullet_key).join(', '.freeze)}")
39
+ Bullet.debug(
40
+ 'Detector::NPlusOneQuery#add_possible_objects',
41
+ "objects: #{objects.map(&:bullet_key).join(', ')}"
42
+ )
35
43
  objects.each { |object| possible_objects.add object.bullet_key }
36
44
  end
37
45
 
38
46
  def add_impossible_object(object)
39
47
  return unless Bullet.start?
40
48
  return unless Bullet.n_plus_one_query_enable?
41
- return unless object.primary_key_value
49
+ return unless object.bullet_primary_key_value
42
50
 
43
- Bullet.debug('Detector::NPlusOneQuery#add_impossible_object'.freeze, "object: #{object.bullet_key}")
51
+ Bullet.debug('Detector::NPlusOneQuery#add_impossible_object', "object: #{object.bullet_key}")
44
52
  impossible_objects.add object.bullet_key
45
53
  end
46
54
 
47
55
  def add_inversed_object(object, association)
48
56
  return unless Bullet.start?
49
57
  return unless Bullet.n_plus_one_query_enable?
50
- return unless object.primary_key_value
58
+ return unless object.bullet_primary_key_value
51
59
 
52
- Bullet.debug('Detector::NPlusOneQuery#add_inversed_object'.freeze, "object: #{object.bullet_key}, association: #{association}")
60
+ Bullet.debug(
61
+ 'Detector::NPlusOneQuery#add_inversed_object',
62
+ "object: #{object.bullet_key}, association: #{association}"
63
+ )
53
64
  inversed_objects.add object.bullet_key, association
54
65
  end
55
66
 
@@ -69,14 +80,13 @@ module Bullet
69
80
  # check if object => associations already exists in object_associations.
70
81
  def association?(object, associations)
71
82
  value = object_associations[object.bullet_key]
72
- if value
73
- value.each do |v|
74
- # associations == v comparison order is important here because
75
- # v variable might be a squeel node where :== method is redefined,
76
- # so it does not compare values at all and return unexpected results
77
- result = v.is_a?(Hash) ? v.key?(associations) : associations == v
78
- return true if result
79
- end
83
+ value&.each do |v|
84
+ # associations == v comparison order is important here because
85
+ # v variable might be a squeel node where :== method is redefined,
86
+ # so it does not compare values at all and return unexpected results
87
+ result =
88
+ v.is_a?(Hash) ? v.key?(associations) : associations == v
89
+ return true if result
80
90
  end
81
91
 
82
92
  false
@@ -6,7 +6,7 @@ module Bullet
6
6
  extend Dependency
7
7
  extend StackTraceFilter
8
8
 
9
- class <<self
9
+ class << self
10
10
  # check if there are unused preload associations.
11
11
  # get related_objects from eager_loadings associated with object and associations
12
12
  # get call_object_association from associations of call_object_associations whose object is in related_objects
@@ -27,9 +27,12 @@ module Bullet
27
27
  def add_eager_loadings(objects, associations)
28
28
  return unless Bullet.start?
29
29
  return unless Bullet.unused_eager_loading_enable?
30
- return if objects.map(&:primary_key_value).compact.empty?
30
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
31
31
 
32
- Bullet.debug('Detector::UnusedEagerLoading#add_eager_loadings', "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}")
32
+ Bullet.debug(
33
+ 'Detector::UnusedEagerLoading#add_eager_loadings',
34
+ "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}"
35
+ )
33
36
  bullet_keys = objects.map(&:bullet_key)
34
37
 
35
38
  to_add = []
@@ -75,6 +78,7 @@ module Bullet
75
78
  eager_loadings.similarly_associated(bullet_key, associations).each do |related_bullet_key|
76
79
  coa = call_object_associations[related_bullet_key]
77
80
  next if coa.nil?
81
+
78
82
  all.merge coa
79
83
  end
80
84
  all.to_a
@@ -2,12 +2,14 @@
2
2
 
3
3
  class Object
4
4
  def bullet_key
5
- "#{self.class}:#{primary_key_value}"
5
+ "#{self.class}:#{bullet_primary_key_value}"
6
6
  end
7
7
 
8
- def primary_key_value
8
+ def bullet_primary_key_value
9
+ return if respond_to?(:persisted?) && !persisted?
10
+
9
11
  if self.class.respond_to?(:primary_keys) && self.class.primary_keys
10
- self.class.primary_keys.map { |primary_key| send primary_key }.join(','.freeze)
12
+ self.class.primary_keys.map { |primary_key| send primary_key }.join(',')
11
13
  elsif self.class.respond_to?(:primary_key) && self.class.primary_key
12
14
  send self.class.primary_key
13
15
  else
@@ -2,6 +2,6 @@
2
2
 
3
3
  class String
4
4
  def bullet_class_name
5
- sub(/:[^:]*?$/, ''.freeze)
5
+ sub(/:[^:]*?$/, '')
6
6
  end
7
7
  end
@@ -23,7 +23,8 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
+
27
28
  records = []
28
29
  origin_each { |record| records << record }
29
30
  if records.length > 1
@@ -36,9 +37,7 @@ module Bullet
36
37
 
37
38
  def eager_load(docs)
38
39
  associations = criteria.inclusions.map(&:name)
39
- docs.each do |doc|
40
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
41
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
42
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
43
42
  origin_eager_load(docs)
44
43
  end
@@ -49,9 +48,7 @@ module Bullet
49
48
 
50
49
  def get_relation(name, metadata, object, reload = false)
51
50
  result = origin_get_relation(name, metadata, object, reload)
52
- if metadata.macro !~ /embed/
53
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
54
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
55
52
  result
56
53
  end
57
54
  end
@@ -23,7 +23,8 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
+
27
28
  records = []
28
29
  origin_each { |record| records << record }
29
30
  if records.length > 1
@@ -36,9 +37,7 @@ module Bullet
36
37
 
37
38
  def eager_load(docs)
38
39
  associations = criteria.inclusions.map(&:name)
39
- docs.each do |doc|
40
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
41
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
42
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
43
42
  origin_eager_load(docs)
44
43
  end
@@ -49,9 +48,7 @@ module Bullet
49
48
 
50
49
  def get_relation(name, metadata, object, reload = false)
51
50
  result = origin_get_relation(name, metadata, object, reload)
52
- if metadata.macro !~ /embed/
53
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
54
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
55
52
  result
56
53
  end
57
54
  end
@@ -10,20 +10,21 @@ module Bullet
10
10
  alias_method :origin_each, :each
11
11
  alias_method :origin_eager_load, :eager_load
12
12
 
13
- def first
14
- result = origin_first
13
+ def first(opt = {})
14
+ result = origin_first(opt)
15
15
  Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
16
  result
17
17
  end
18
18
 
19
- def last
20
- result = origin_last
19
+ def last(opt = {})
20
+ result = origin_last(opt)
21
21
  Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
22
  result
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
+
27
28
  records = []
28
29
  origin_each { |record| records << record }
29
30
  if records.length > 1
@@ -36,9 +37,7 @@ module Bullet
36
37
 
37
38
  def eager_load(docs)
38
39
  associations = criteria.inclusions.map(&:name)
39
- docs.each do |doc|
40
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
41
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
42
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
43
42
  origin_eager_load(docs)
44
43
  end
@@ -49,9 +48,7 @@ module Bullet
49
48
 
50
49
  def get_relation(name, metadata, object, reload = false)
51
50
  result = origin_get_relation(name, metadata, object, reload)
52
- if metadata.macro !~ /embed/
53
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
54
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
55
52
  result
56
53
  end
57
54
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module Mongoid
5
+ def self.enable
6
+ require 'mongoid'
7
+ ::Mongoid::Contextual::Mongo.class_eval do
8
+ alias_method :origin_first, :first
9
+ alias_method :origin_last, :last
10
+ alias_method :origin_each, :each
11
+ alias_method :origin_eager_load, :eager_load
12
+
13
+ def first(opts = {})
14
+ result = origin_first(opts)
15
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
+ result
17
+ end
18
+
19
+ def last(opts = {})
20
+ result = origin_last(opts)
21
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
+ result
23
+ end
24
+
25
+ def each(&block)
26
+ return to_enum unless block
27
+
28
+ records = []
29
+ origin_each { |record| records << record }
30
+ if records.length > 1
31
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
32
+ elsif records.size == 1
33
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
34
+ end
35
+ records.each(&block)
36
+ end
37
+
38
+ def eager_load(docs)
39
+ associations = criteria.inclusions.map(&:name)
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
41
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
42
+ origin_eager_load(docs)
43
+ end
44
+ end
45
+
46
+ ::Mongoid::Association::Accessors.class_eval do
47
+ alias_method :origin_get_relation, :get_relation
48
+
49
+ def get_relation(name, association, object, reload = false)
50
+ result = origin_get_relation(name, association, object, reload)
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
52
+ result
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end