bullet 2.0.0.beta.2 → 2.0.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 (41) hide show
  1. data/MIT-LICENSE +1 -1
  2. data/README.textile +38 -8
  3. data/README_for_rails2.textile +19 -3
  4. data/lib/bullet/action_controller2.rb +4 -4
  5. data/lib/bullet/active_record2.rb +16 -16
  6. data/lib/bullet/active_record3.rb +16 -25
  7. data/lib/bullet/detector/association.rb +135 -0
  8. data/lib/bullet/detector/base.rb +19 -0
  9. data/lib/bullet/detector/counter.rb +43 -0
  10. data/lib/bullet/detector/n_plus_one_query.rb +39 -0
  11. data/lib/bullet/detector/unused_eager_association.rb +39 -0
  12. data/lib/bullet/detector.rb +9 -0
  13. data/lib/bullet/notification/base.rb +57 -0
  14. data/lib/bullet/notification/counter_cache.rb +13 -0
  15. data/lib/bullet/notification/n_plus_one_query.rb +32 -0
  16. data/lib/bullet/notification/unused_eager_loading.rb +14 -0
  17. data/lib/bullet/notification.rb +4 -79
  18. data/lib/bullet/notification_collector.rb +25 -0
  19. data/lib/bullet/rack.rb +44 -0
  20. data/lib/bullet/registry/association.rb +16 -0
  21. data/lib/bullet/registry/base.rb +39 -0
  22. data/lib/bullet/registry/object.rb +15 -0
  23. data/lib/bullet/registry.rb +7 -0
  24. data/lib/bullet/version.rb +5 -0
  25. data/lib/bullet.rb +63 -42
  26. metadata +60 -42
  27. data/Rakefile +0 -34
  28. data/VERSION +0 -1
  29. data/bullet.gemspec +0 -67
  30. data/lib/bullet/association.rb +0 -294
  31. data/lib/bullet/counter.rb +0 -101
  32. data/lib/bullet/logger.rb +0 -9
  33. data/lib/bulletware.rb +0 -42
  34. data/rails/init.rb +0 -1
  35. data/spec/bullet/association_for_chris_spec.rb +0 -96
  36. data/spec/bullet/association_for_peschkaj_spec.rb +0 -86
  37. data/spec/bullet/association_spec.rb +0 -1043
  38. data/spec/bullet/counter_spec.rb +0 -136
  39. data/spec/spec.opts +0 -3
  40. data/spec/spec_helper.rb +0 -45
  41. data/tasks/bullet_tasks.rake +0 -9
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com)
1
+ Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.textile CHANGED
@@ -17,6 +17,7 @@ You can install it as a gem:
17
17
  <pre><code>
18
18
  sudo gem install bullet --pre
19
19
  </code></pre>
20
+
20
21
  ****************************************************************************
21
22
 
22
23
  h2. Configuration
@@ -29,6 +30,10 @@ config.after_initialize do
29
30
  Bullet.bullet_logger = true
30
31
  Bullet.console = true
31
32
  Bullet.growl = true
33
+ Bullet.xmpp = { :account => 'bullets_account@jabber.org',
34
+ :password => 'bullets_password_for_jabber',
35
+ :receiver => 'your_account@jabber.org',
36
+ :show_online_status => true }
32
37
  Bullet.rails_logger = true
33
38
  Bullet.disable_browser_cache = true
34
39
  end
@@ -43,13 +48,27 @@ It is recommended to config growl notification as follows if your collaborators
43
48
  end
44
49
  </code></pre>
45
50
 
46
- The code above will enable all five of the Bullet notification systems:
51
+ and similarly for XMPP:
52
+ <pre><code>
53
+ begin
54
+ require 'xmpp4r'
55
+ Bullet.xmpp = { :account => 'bullets_account@jabber.org',
56
+ :password => 'bullets_password_for_jabber',
57
+ :receiver => 'your_account@jabber.org',
58
+ :show_online_status => true }
59
+ rescue MissingSourceFile
60
+ end
61
+ </code></pre>
62
+
63
+
64
+ The code above will enable all six of the Bullet notification systems:
47
65
  * <code>Bullet.enable</code>: enable Bullet plugin/gem, otherwise do nothing
48
66
  * <code>Bullet.alert</code>: pop up a JavaScript alert in the browser
49
67
  * <code>Bullet.bullet_logger</code>: log to the Bullet log file (Rails.root/log/bullet.log)
50
68
  * <code>Bullet.rails_logger</code>: add warnings directly to the Rails log
51
69
  * <code>Bullet.console</code>: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed)
52
70
  * <code>Bullet.growl</code>: pop up Growl warnings if your system has Growl installed. Requires a little bit of configuration
71
+ * <code>Bullet.xmpp</code>: send XMPP/Jabber notifications to the receiver indicated. Note that the code will currently not handle the adding of contacts, so you will need to make both accounts indicated know each other manually before you will receive any notifications. If you restart the development server frequently, the 'coming online' sound for the bullet account may start to annoy - in this case set :show_online_status to false; you will still get notifications, but the bullet account won't announce it's online status anymore.
53
72
  * <code>Bullet.disable_browser_cache</code>: disable browser cache which usually causes unexpected problems
54
73
 
55
74
  ****************************************************************************
@@ -105,6 +124,17 @@ ruby-growl gem has an issue about md5 in ruby 1.9, if you use growl and ruby 1.9
105
124
 
106
125
  ****************************************************************************
107
126
 
127
+ h2. XMPP/Jabber Support
128
+
129
+ To get XMPP support up-and-running for Bullet, follow the steps below:
130
+ * Install the xmpp4r gem: <code>sudo gem install xmpp4r</code>
131
+ * Make both the bullet and the receipient account add each other as contacts.
132
+ This will require you to manually log into both accounts, add each other
133
+ as contact and confirm each others contact request.
134
+ * Boot up your application. Bullet will automatically send an XMPP notification when XMPP is turned on.
135
+
136
+ ****************************************************************************
137
+
108
138
  h2. Important
109
139
 
110
140
  If you find bullet does not work for you, *please disable your browser's cache*.
@@ -134,8 +164,7 @@ end
134
164
 
135
165
  after(:each)
136
166
  if Bullet.enable?
137
- Bullet.growl_notification
138
- Bullet.log_notification('Test: ')
167
+ Bullet.perform_out_of_channel_notifications
139
168
  Bullet.end_request
140
169
  end
141
170
  end
@@ -155,6 +184,7 @@ h2. Links
155
184
 
156
185
  h2. Contributors
157
186
 
187
+ sriedel did a lot of awesome refactors, added xmpp notification support, and added Hacking.textile about how to extend to bullet plugin.
158
188
  flipsasser added Growl, console.log and Rails.log support, very awesome. And he also improved README.
159
189
  rainux added group style console.log.
160
190
  2collegebums added some great specs to generate red bar.
@@ -203,7 +233,7 @@ post2.comments.create(:name => 'fourth')
203
233
  <pre><code>
204
234
  <% @posts.each do |post| %>
205
235
  <tr>
206
- <td><%=h post.name %></td>
236
+ <td><%= post.name %></td>
207
237
  <td><%= post.comments.collect(&:name) %></td>
208
238
  <td><%= link_to 'Show', post %></td>
209
239
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
@@ -315,7 +345,7 @@ a N+1 query fixed. Cool!
315
345
  <pre><code>
316
346
  <% @posts.each do |post| %>
317
347
  <tr>
318
- <td><%=h post.name %></td>
348
+ <td><%= post.name %></td>
319
349
  <td><%= link_to 'Show', post %></td>
320
350
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
321
351
  <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
@@ -355,8 +385,8 @@ Remove from your finder: :include => [:comments]
355
385
  <pre><code>
356
386
  <% @posts.each do |post| %>
357
387
  <tr>
358
- <td><%=h post.name %></td>
359
- <td><%=h post.comments.size %></td>
388
+ <td><%= post.name %></td>
389
+ <td><%= post.comments.size %></td>
360
390
  <td><%= link_to 'Show', post %></td>
361
391
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
362
392
  <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
@@ -381,4 +411,4 @@ In the meanwhile, there's a log appended into <code>log/bullet.log</code> file.
381
411
  ****************************************************************************
382
412
 
383
413
 
384
- Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com), released under the MIT license
414
+ Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com), released under the MIT license
@@ -18,6 +18,7 @@ There is a large refactor from gem 1.4 to 1.5, so if you upgrade to 1.5 gem, ple
18
18
 
19
19
  h2. Contributors
20
20
 
21
+ sriedel did a lot of awesome refactors, added xmpp notification support, and added Hacking.textile about how to extend to bullet plugin.
21
22
  flipsasser added Growl, console.log and Rails.log support, very awesome. And he also improved README.
22
23
  rainux added group style console.log.
23
24
  2collegebums added some great specs to generate red bar.
@@ -52,6 +53,10 @@ config.after_initialize do
52
53
  Bullet.growl = true
53
54
  Bullet.rails_logger = true
54
55
  Bullet.disable_browser_cache = true
56
+ Bullet.xmpp = { :account => 'bullets_account@jabber.org',
57
+ :password => 'bullets_password_for_jabber',
58
+ :receiver => 'your_account@jabber.org',
59
+ :show_online_status => true }
55
60
  end
56
61
  </code></pre>
57
62
 
@@ -64,6 +69,18 @@ It is recommended to config growl notification as follows if your collaborators
64
69
  end
65
70
  </code></pre>
66
71
 
72
+ and similarly for XMPP:
73
+ <pre><code>
74
+ begin
75
+ require 'xmpp4r'
76
+ Bullet.xmpp = { :account => 'bullets_account@jabber.org',
77
+ :password => 'bullets_password_for_jabber',
78
+ :receiver => 'your_account@jabber.org',
79
+ :show_online_status => true }
80
+ rescue MissingSourceFile
81
+ end
82
+ </code></pre>
83
+
67
84
  The code above will enable all five of the Bullet notification systems:
68
85
  * <code>Bullet.enable</code>: enable Bullet plugin/gem, otherwise do nothing
69
86
  * <code>Bullet.alert</code>: pop up a JavaScript alert in the browser
@@ -167,8 +184,7 @@ end
167
184
 
168
185
  after(:each)
169
186
  if Bullet.enable?
170
- Bullet.growl_notification
171
- Bullet.log_notification('Test: ')
187
+ Bullet.perform_out_of_channel_notifications
172
188
  Bullet.end_request
173
189
  end
174
190
  end
@@ -401,4 +417,4 @@ In the meanwhile, there's a log appended into <code>log/bullet.log</code> file.
401
417
  ****************************************************************************
402
418
 
403
419
 
404
- Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com), released under the MIT license
420
+ Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com), released under the MIT license
@@ -3,7 +3,8 @@ module Bullet
3
3
  def self.enable
4
4
  require 'action_controller'
5
5
  case Rails.version
6
- when /^2.3/
6
+ when /^2.3/
7
+ ::ActionController::Dispatcher.middleware.use Bullet::Rack
7
8
  ::ActionController::Dispatcher.class_eval do
8
9
  class <<self
9
10
  alias_method :origin_reload_application, :reload_application
@@ -30,12 +31,11 @@ module Bullet
30
31
 
31
32
  if Bullet.notification?
32
33
  if response.headers["type"] and response.headers["type"].include? 'text/html' and response.body =~ %r{<html.*</html>}m
33
- response.body <<= Bullet.javascript_notification
34
+ response.body <<= Bullet.gather_inline_notifications
34
35
  response.headers["Content-Length"] = response.body.length.to_s
35
36
  end
36
37
 
37
- Bullet.growl_notification
38
- Bullet.log_notification(request.params['PATH_INFO'])
38
+ Bullet.perform_bullet_out_of_channel_notifications
39
39
  end
40
40
  Bullet.end_request
41
41
  response
@@ -12,11 +12,11 @@ module Bullet
12
12
 
13
13
  if records
14
14
  if records.size > 1
15
- Bullet::Association.add_possible_objects(records)
16
- Bullet::Counter.add_possible_objects(records)
15
+ Bullet::Detector::Association.add_possible_objects(records)
16
+ Bullet::Detector::Counter.add_possible_objects(records)
17
17
  elsif records.size == 1
18
- Bullet::Association.add_impossible_object(records.first)
19
- Bullet::Counter.add_impossible_object(records.first)
18
+ Bullet::Detector::Association.add_impossible_object(records.first)
19
+ Bullet::Detector::Counter.add_impossible_object(records.first)
20
20
  end
21
21
  end
22
22
 
@@ -33,9 +33,9 @@ module Bullet
33
33
  records = [records].flatten.compact.uniq
34
34
  return if records.empty?
35
35
  records.each do |record|
36
- Bullet::Association.add_object_associations(record, associations)
36
+ Bullet::Detector::Association.add_object_associations(record, associations)
37
37
  end
38
- Bullet::Association.add_eager_loadings(records, associations)
38
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
39
39
  origin_preload_associations(records, associations, preload_options={})
40
40
  end
41
41
  end
@@ -47,10 +47,10 @@ module Bullet
47
47
  records = origin_find_with_associations(options)
48
48
  associations = merge_includes(scope(:find, :include), options[:include])
49
49
  records.each do |record|
50
- Bullet::Association.add_object_associations(record, associations)
51
- Bullet::Association.call_association(record, associations)
50
+ Bullet::Detector::Association.add_object_associations(record, associations)
51
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
52
52
  end
53
- Bullet::Association.add_eager_loadings(records, associations)
53
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
54
54
  records
55
55
  end
56
56
  end
@@ -60,8 +60,8 @@ module Bullet
60
60
  alias_method :origin_construct_association, :construct_association
61
61
  def construct_association(record, join, row)
62
62
  associations = join.reflection.name
63
- Bullet::Association.add_object_associations(record, associations)
64
- Bullet::Association.call_association(record, associations)
63
+ Bullet::Detector::Association.add_object_associations(record, associations)
64
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
65
65
  origin_construct_association(record, join, row)
66
66
  end
67
67
  end
@@ -70,7 +70,7 @@ module Bullet
70
70
  # call one to many associations
71
71
  alias_method :origin_load_target, :load_target
72
72
  def load_target
73
- Bullet::Association.call_association(@owner, @reflection.name)
73
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
74
74
  origin_load_target
75
75
  end
76
76
  end
@@ -81,8 +81,8 @@ module Bullet
81
81
  def load_target
82
82
  # avoid stack level too deep
83
83
  result = origin_load_target
84
- Bullet::Association.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
85
- Bullet::Association.add_possible_objects(result)
84
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
85
+ Bullet::Detector::Association.add_possible_objects(result)
86
86
  result
87
87
  end
88
88
  end
@@ -91,7 +91,7 @@ module Bullet
91
91
  alias_method :origin_has_cached_counter?, :has_cached_counter?
92
92
  def has_cached_counter?
93
93
  result = origin_has_cached_counter?
94
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
94
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
95
95
  result
96
96
  end
97
97
  end
@@ -100,7 +100,7 @@ module Bullet
100
100
  alias_method :origin_has_cached_counter?, :has_cached_counter?
101
101
  def has_cached_counter?
102
102
  result = origin_has_cached_counter?
103
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
103
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
104
104
  result
105
105
  end
106
106
  end
@@ -9,11 +9,11 @@ module Bullet
9
9
  def to_a
10
10
  records = origin_to_a
11
11
  if records.size > 1
12
- Bullet::Association.add_possible_objects(records)
13
- Bullet::Counter.add_possible_objects(records)
12
+ Bullet::Detector::Association.add_possible_objects(records)
13
+ Bullet::Detector::Counter.add_possible_objects(records)
14
14
  elsif records.size == 1
15
- Bullet::Association.add_impossible_object(records.first)
16
- Bullet::Counter.add_impossible_object(records.first)
15
+ Bullet::Detector::Association.add_impossible_object(records.first)
16
+ Bullet::Detector::Counter.add_impossible_object(records.first)
17
17
  end
18
18
  records
19
19
  end
@@ -27,9 +27,9 @@ module Bullet
27
27
  records = [records].flatten.compact.uniq
28
28
  return if records.empty?
29
29
  records.each do |record|
30
- Bullet::Association.add_object_associations(record, associations)
30
+ Bullet::Detector::Association.add_object_associations(record, associations)
31
31
  end
32
- Bullet::Association.add_eager_loadings(records, associations)
32
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
33
33
  origin_preload_associations(records, associations, preload_options={})
34
34
  end
35
35
  end
@@ -41,10 +41,10 @@ module Bullet
41
41
  records = origin_find_with_associations
42
42
  associations = (@eager_load_values + @includes_values).uniq
43
43
  records.each do |record|
44
- Bullet::Association.add_object_associations(record, associations)
45
- Bullet::Association.call_association(record, associations)
44
+ Bullet::Detector::Association.add_object_associations(record, associations)
45
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
46
46
  end
47
- Bullet::Association.add_eager_loadings(records, associations)
47
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
48
48
  records
49
49
  end
50
50
  end
@@ -54,8 +54,8 @@ module Bullet
54
54
  # call join associations
55
55
  def construct_association(record, join, row)
56
56
  associations = join.reflection.name
57
- Bullet::Association.add_object_associations(record, associations)
58
- Bullet::Association.call_association(record, associations)
57
+ Bullet::Detector::Association.add_object_associations(record, associations)
58
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
59
59
  origin_construct_association(record, join, row)
60
60
  end
61
61
  end
@@ -64,7 +64,7 @@ module Bullet
64
64
  # call one to many associations
65
65
  alias_method :origin_load_target, :load_target
66
66
  def load_target
67
- Bullet::Association.call_association(@owner, @reflection.name)
67
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
68
68
  origin_load_target
69
69
  end
70
70
  end
@@ -75,8 +75,8 @@ module Bullet
75
75
  def load_target
76
76
  # avoid stack level too deep
77
77
  result = origin_load_target
78
- Bullet::Association.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
79
- Bullet::Association.add_possible_objects(result)
78
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
79
+ Bullet::Detector::Association.add_possible_objects(result)
80
80
  result
81
81
  end
82
82
  end
@@ -86,7 +86,7 @@ module Bullet
86
86
 
87
87
  def has_cached_counter?
88
88
  result = origin_has_cached_counter?
89
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
89
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
90
90
  result
91
91
  end
92
92
  end
@@ -95,16 +95,7 @@ module Bullet
95
95
  alias_method :origin_has_cached_counter?, :has_cached_counter?
96
96
  def has_cached_counter?
97
97
  result = origin_has_cached_counter?
98
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
99
- result
100
- end
101
- end
102
-
103
- ::ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
104
- alias_method :origin_has_cached_counter?, :has_cached_counter?
105
- def has_cached_counter?
106
- result = origin_has_cached_counter?
107
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
98
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
108
99
  result
109
100
  end
110
101
  end
@@ -0,0 +1,135 @@
1
+ module Bullet
2
+ module Detector
3
+ class Association < Base
4
+ class <<self
5
+ def start_request
6
+ @@checked = false
7
+ end
8
+
9
+ def clear
10
+ # Note that under ruby class variables are shared among the class
11
+ # that declares them and all classes derived from that class.
12
+ # The following variables are accessible by all classes that
13
+ # derive from Bullet::Detector::Association - changing the variable
14
+ # in one subclass will make the change visible to all subclasses!
15
+ @@object_associations = nil
16
+ @@callers = nil
17
+ @@possible_objects = nil
18
+ @@impossible_objects = nil
19
+ @@call_object_associations = nil
20
+ @@eager_loadings = nil
21
+ end
22
+
23
+ def add_object_associations(object, associations)
24
+ object_associations.add( object, associations )
25
+ end
26
+
27
+ def add_call_object_associations(object, associations)
28
+ call_object_associations.add( object, associations )
29
+ end
30
+
31
+ def add_possible_objects(objects)
32
+ possible_objects.add objects
33
+ end
34
+
35
+ def add_impossible_object(object)
36
+ impossible_objects.add object
37
+ end
38
+
39
+ def add_eager_loadings(objects, associations)
40
+ objects = Array(objects)
41
+
42
+ eager_loadings.each do |k, v|
43
+ key_objects_overlap = k & objects
44
+
45
+ next if key_objects_overlap.empty?
46
+
47
+ if key_objects_overlap == k
48
+ eager_loadings.add k, associations
49
+ break
50
+
51
+ else
52
+ eager_loadings.merge key_objects_overlap, ( eager_loadings[k].dup << associations )
53
+
54
+ keys_without_objects = k - objects
55
+ eager_loadings.merge keys_without_objects, eager_loadings[k] unless keys_without_objects.empty?
56
+
57
+ eager_loadings.delete(k)
58
+ objects = objects - k
59
+ end
60
+ end
61
+
62
+ eager_loadings.add objects, associations unless objects.empty?
63
+ end
64
+
65
+ private
66
+ def possible?(object)
67
+ possible_objects.contains? object
68
+ end
69
+
70
+ def impossible?(object)
71
+ impossible_objects.contains? object
72
+ end
73
+
74
+ # check if object => associations already exists in object_associations.
75
+ def association?(object, associations)
76
+ object_associations.each do |key, value|
77
+ next unless key == object
78
+
79
+ value.each do |v|
80
+ result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
81
+ return true if result
82
+ end
83
+
84
+ end
85
+ return false
86
+ end
87
+
88
+ # object_associations keep the object relationships
89
+ # that the object has many associations.
90
+ # e.g. { <Post id:1> => [:comments] }
91
+ # the object_associations keep all associations that may be or may no be
92
+ # unpreload associations or unused preload associations.
93
+ def object_associations
94
+ @@object_associations ||= Bullet::Registry::Base.new
95
+ end
96
+
97
+ # call_object_assciations keep the object relationships
98
+ # that object.associations is called.
99
+ # e.g. { <Post id:1> => [:comments] }
100
+ # they are used to detect unused preload associations.
101
+ def call_object_associations
102
+ @@call_object_associations ||= Bullet::Registry::Base.new
103
+ end
104
+
105
+ # possible_objects keep the class to object relationships
106
+ # that the objects may cause N+1 query.
107
+ # e.g. { Post => [<Post id:1>, <Post id:2>] }
108
+ def possible_objects
109
+ @@possible_objects ||= Bullet::Registry::Object.new
110
+ end
111
+
112
+ # impossible_objects keep the class to objects relationships
113
+ # that the objects may not cause N+1 query.
114
+ # e.g. { Post => [<Post id:1>, <Post id:2>] }
115
+ # Notice: impossible_objects are not accurate,
116
+ # if find collection returns only one object, then the object is impossible object,
117
+ # impossible_objects are used to avoid treating 1+1 query to N+1 query.
118
+ def impossible_objects
119
+ @@impossible_objects ||= Bullet::Registry::Object.new
120
+ end
121
+
122
+ # eager_loadings keep the object relationships
123
+ # that the associations are preloaded by find :include.
124
+ # e.g. { [<Post id:1>, <Post id:2>] => [:comments, :user] }
125
+ def eager_loadings
126
+ @@eager_loadings ||= Bullet::Registry::Association.new
127
+ end
128
+
129
+ def callers
130
+ @@callers ||= []
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,19 @@
1
+ module Bullet
2
+ module Detector
3
+ class Base
4
+ def self.start_request
5
+ end
6
+
7
+ def self.end_request
8
+ clear
9
+ end
10
+
11
+ protected
12
+ def self.unique( array )
13
+ array.flatten!
14
+ array.uniq!
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module Bullet
2
+ module Detector
3
+ class Counter < Base
4
+ def self.clear
5
+ @@possible_objects = nil
6
+ @@impossible_objects = nil
7
+ end
8
+
9
+ def self.add_counter_cache(object, associations)
10
+ if conditions_met?( object, associations )
11
+ create_notification object.class, associations
12
+ end
13
+ end
14
+
15
+ def self.add_possible_objects(objects)
16
+ possible_objects.add objects
17
+ end
18
+
19
+ def self.add_impossible_object(object)
20
+ impossible_objects.add object
21
+ end
22
+
23
+ private
24
+ def self.create_notification( klazz, associations )
25
+ notice = Bullet::Notification::CounterCache.new klazz, associations
26
+ Bullet.notification_collector.add notice
27
+ end
28
+
29
+ def self.possible_objects
30
+ @@possible_objects ||= Bullet::Registry::Object.new
31
+ end
32
+
33
+ def self.impossible_objects
34
+ @@impossible_objects ||= Bullet::Registry::Object.new
35
+ end
36
+
37
+ def self.conditions_met?( object, associations )
38
+ possible_objects.contains?( object ) and
39
+ !impossible_objects.contains?( object )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module Bullet
2
+ module Detector
3
+ class NPlusOneQuery < Association
4
+ # executed when object.assocations is called.
5
+ # first, it keeps this method call for object.association.
6
+ # then, it checks if this associations call is unpreload.
7
+ # if it is, keeps this unpreload associations and caller.
8
+ def self.call_association(object, associations)
9
+ @@checked = true
10
+ add_call_object_associations(object, associations)
11
+
12
+ if conditions_met?(object, associations)
13
+ caller_in_project
14
+ create_notification object.class, associations
15
+ end
16
+ end
17
+
18
+ private
19
+ def self.create_notification(klazz, associations)
20
+ notice = Bullet::Notification::NPlusOneQuery.new( callers, klazz, associations )
21
+ Bullet.notification_collector.add( notice )
22
+ end
23
+
24
+ # decide whether the object.associations is unpreloaded or not.
25
+ def self.conditions_met?(object, associations)
26
+ possible?(object) and
27
+ !impossible?(object) and
28
+ !association?(object, associations)
29
+ end
30
+
31
+ def self.caller_in_project
32
+ vender_root ||= File.join(Rails.root, 'vendor')
33
+ callers << caller.select { |c| c =~ /#{Rails.root}/ }.
34
+ reject { |c| c =~ /#{vender_root}/ }
35
+ callers.uniq!
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ module Bullet
2
+ module Detector
3
+ class UnusedEagerAssociation < Association
4
+ # check if there are unused preload associations.
5
+ # for each object => association
6
+ # get related_objects from eager_loadings associated with object and associations
7
+ # get call_object_association from associations of call_object_associations whose object is in related_objects
8
+ # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
9
+ def self.check_unused_preload_associations
10
+ @@checked = true
11
+ object_associations.each do |object, association|
12
+ object_association_diff = diff_object_association object, association
13
+ next if object_association_diff.empty?
14
+
15
+ create_notification object.class, object_association_diff
16
+ end
17
+ end
18
+
19
+ protected
20
+ def self.create_notification(klazz, associations)
21
+ notice = Bullet::Notification::UnusedEagerLoading.new( klazz, associations )
22
+ Bullet.notification_collector.add( notice )
23
+ end
24
+
25
+ def self.call_object_association( object, association )
26
+ eager_loadings.similarly_associated( object, association ).
27
+ collect { |related_object| call_object_associations[related_object] }.
28
+ compact.
29
+ flatten.
30
+ uniq
31
+ end
32
+
33
+ def self.diff_object_association( object, association )
34
+ potential_objects = association - call_object_association( object, association )
35
+ potential_objects.reject {|a| a.is_a?( Hash ) }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ module Bullet
2
+ module Detector
3
+ autoload :Base, 'bullet/detector/base'
4
+ autoload :Association, 'bullet/detector/association'
5
+ autoload :NPlusOneQuery, 'bullet/detector/n_plus_one_query'
6
+ autoload :UnusedEagerAssociation, 'bullet/detector/unused_eager_association'
7
+ autoload :Counter, 'bullet/detector/counter'
8
+ end
9
+ end