bullet 2.0.0.beta.2 → 2.0.0.beta.3

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 (49) hide show
  1. data/.rspec +1 -0
  2. data/Hacking.textile +100 -0
  3. data/README.textile +36 -5
  4. data/README_for_rails2.textile +17 -0
  5. data/Rakefile +33 -16
  6. data/VERSION +1 -1
  7. data/autotest/discover.rb +1 -0
  8. data/bullet.gemspec +32 -9
  9. data/lib/bullet.rb +69 -38
  10. data/lib/bullet/action_controller2.rb +4 -4
  11. data/lib/bullet/active_record2.rb +16 -16
  12. data/lib/bullet/active_record3.rb +16 -25
  13. data/lib/bullet/detector.rb +9 -0
  14. data/lib/bullet/detector/association.rb +135 -0
  15. data/lib/bullet/detector/base.rb +19 -0
  16. data/lib/bullet/detector/counter.rb +43 -0
  17. data/lib/bullet/detector/n_plus_one_query.rb +39 -0
  18. data/lib/bullet/detector/unused_eager_association.rb +39 -0
  19. data/lib/bullet/notification.rb +4 -79
  20. data/lib/bullet/notification/base.rb +59 -0
  21. data/lib/bullet/notification/counter_cache.rb +13 -0
  22. data/lib/bullet/notification/n_plus_one_query.rb +32 -0
  23. data/lib/bullet/notification/unused_eager_loading.rb +14 -0
  24. data/lib/bullet/notification_collector.rb +25 -0
  25. data/lib/bullet/presenter.rb +13 -0
  26. data/lib/bullet/presenter/base.rb +9 -0
  27. data/lib/bullet/presenter/bullet_logger.rb +28 -0
  28. data/lib/bullet/presenter/growl.rb +40 -0
  29. data/lib/bullet/presenter/javascript_alert.rb +15 -0
  30. data/lib/bullet/presenter/javascript_console.rb +28 -0
  31. data/lib/bullet/presenter/javascript_helpers.rb +13 -0
  32. data/lib/bullet/presenter/rails_logger.rb +15 -0
  33. data/lib/bullet/presenter/xmpp.rb +56 -0
  34. data/lib/bullet/rack.rb +42 -0
  35. data/lib/bullet/registry.rb +7 -0
  36. data/lib/bullet/registry/association.rb +16 -0
  37. data/lib/bullet/registry/base.rb +39 -0
  38. data/lib/bullet/registry/object.rb +15 -0
  39. data/spec/bullet/association_for_chris_spec.rb +6 -6
  40. data/spec/bullet/association_for_peschkaj_spec.rb +6 -6
  41. data/spec/bullet/association_spec.rb +118 -262
  42. data/spec/bullet/counter_spec.rb +10 -10
  43. data/spec/spec_helper.rb +51 -17
  44. metadata +32 -9
  45. data/lib/bullet/association.rb +0 -294
  46. data/lib/bullet/counter.rb +0 -101
  47. data/lib/bullet/logger.rb +0 -9
  48. data/lib/bulletware.rb +0 -42
  49. data/spec/spec.opts +0 -3
@@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
4
4
 
5
- describe Bullet::Counter do
5
+ describe Bullet::Detector::Counter do
6
6
  def setup_db
7
7
  ActiveRecord::Schema.define(:version => 1) do
8
8
  create_table :countries do |t|
@@ -47,34 +47,34 @@ describe Bullet::Counter do
47
47
  end
48
48
 
49
49
  before(:each) do
50
- Bullet::Counter.start_request
50
+ Bullet.start_request
51
51
  end
52
52
 
53
53
  after(:each) do
54
- Bullet::Counter.end_request
54
+ Bullet.end_request
55
55
  end
56
56
 
57
57
  it "should need counter cache with all cities" do
58
58
  Country.all.each do |country|
59
59
  country.cities.size
60
60
  end
61
- Bullet::Counter.should be_need_counter_caches
61
+ Bullet.collected_counter_cache_notifications.should_not be_empty
62
62
  end
63
63
 
64
64
  it "should not need coounter cache with only one object" do
65
65
  Country.first.cities.size
66
- Bullet::Counter.should_not be_need_counter_caches
66
+ Bullet.collected_counter_cache_notifications.should be_empty
67
67
  end
68
68
 
69
69
  it "should not need counter cache with part of cities" do
70
70
  Country.all.each do |country|
71
71
  country.cities.where(:name => 'first').size
72
72
  end
73
- Bullet::Counter.should_not be_need_counter_caches
73
+ Bullet.collected_counter_cache_notifications.should be_empty
74
74
  end
75
75
  end
76
76
 
77
- describe Bullet::Counter do
77
+ describe Bullet::Detector::Counter do
78
78
  def setup_db
79
79
  ActiveRecord::Schema.define(:version => 1) do
80
80
  create_table :people do |t|
@@ -120,17 +120,17 @@ describe Bullet::Counter do
120
120
  end
121
121
 
122
122
  before(:each) do
123
- Bullet::Counter.start_request
123
+ Bullet.start_request
124
124
  end
125
125
 
126
126
  after(:each) do
127
- Bullet::Counter.end_request
127
+ Bullet.end_request
128
128
  end
129
129
 
130
130
  it "should not need counter cache" do
131
131
  Person.all.each do |person|
132
132
  person.pets.size
133
133
  end
134
- Bullet::Counter.should_not be_need_counter_caches
134
+ Bullet.collected_counter_cache_notifications.should be_empty
135
135
  end
136
136
  end
data/spec/spec_helper.rb CHANGED
@@ -19,26 +19,60 @@ Bullet.enable = true
19
19
  ActiveRecord::Migration.verbose = false
20
20
 
21
21
  module Bullet
22
- class Association
23
- class <<self
24
- # returns true if all associations are preloaded
25
- def completely_preloading_associations?
26
- !has_unpreload_associations?
27
- end
22
+ def self.collected_notifications_of_class( notification_class )
23
+ Bullet.notification_collector.collection.select do |notification|
24
+ notification.is_a? notification_class
25
+ end
26
+ end
28
27
 
29
- # returns true if a given object has a specific association
30
- def creating_object_association_for?(object, association)
31
- object_associations[object].present? && object_associations[object].include?(association)
32
- end
28
+ def self.collected_counter_cache_notifications
29
+ collected_notifications_of_class Bullet::Notification::CounterCache
30
+ end
33
31
 
34
- # returns true if a given class includes the specific unpreloaded association
35
- def detecting_unpreloaded_association_for?(klazz, association)
36
- unpreload_associations[klazz].present? && unpreload_associations[klazz].include?(association)
37
- end
32
+ def self.collected_n_plus_one_query_notifications
33
+ collected_notifications_of_class Bullet::Notification::NPlusOneQuery
34
+ end
35
+
36
+ def self.collected_unused_eager_association_notifications
37
+ collected_notifications_of_class Bullet::Notification::UnusedEagerLoading
38
+ end
39
+ end
40
+
41
+ module Bullet
42
+ module Detector
43
+ class Association
44
+ class <<self
45
+ # returns true if all associations are preloaded
46
+ def completely_preloading_associations?
47
+ Bullet.collected_n_plus_one_query_notifications.empty?
48
+ end
49
+
50
+ def has_unused_preload_associations?
51
+ Bullet.collected_unused_eager_association_notifications.present?
52
+ end
53
+
54
+ # returns true if a given object has a specific association
55
+ def creating_object_association_for?(object, association)
56
+ object_associations[object].present? && object_associations[object].include?(association)
57
+ end
58
+
59
+ # returns true if a given class includes the specific unpreloaded association
60
+ def detecting_unpreloaded_association_for?(klass, association)
61
+ for_class_and_assoc = Bullet.collected_n_plus_one_query_notifications.select do |notification|
62
+ notification.base_class == klass and
63
+ notification.associations.include?( association )
64
+ end
65
+ for_class_and_assoc.present?
66
+ end
38
67
 
39
- # returns true if the given class includes the specific unused preloaded association
40
- def unused_preload_associations_for?(klazz, association)
41
- unused_preload_associations[klazz].present? && unused_preload_associations[klazz].include?(association)
68
+ # returns true if the given class includes the specific unused preloaded association
69
+ def unused_preload_associations_for?(klazz, association)
70
+ for_class_and_assoc = Bullet.collected_unused_eager_association_notifications.select do |notification|
71
+ notification.base_class == klass and
72
+ notification.associations.include?( association )
73
+ end
74
+ for_class_and_assoc.present?
75
+ end
42
76
  end
43
77
  end
44
78
  end
metadata CHANGED
@@ -7,8 +7,8 @@ version: !ruby/object:Gem::Version
7
7
  - 0
8
8
  - 0
9
9
  - beta
10
- - 2
11
- version: 2.0.0.beta.2
10
+ - 3
11
+ version: 2.0.0.beta.3
12
12
  platform: ruby
13
13
  authors:
14
14
  - Richard Huang
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2010-03-07 00:00:00 +08:00
19
+ date: 2010-07-14 00:00:00 +08:00
20
20
  default_executable:
21
21
  dependencies: []
22
22
 
@@ -30,27 +30,50 @@ extra_rdoc_files:
30
30
  - README.textile
31
31
  - README_for_rails2.textile
32
32
  files:
33
+ - .rspec
34
+ - Hacking.textile
33
35
  - MIT-LICENSE
34
36
  - README.textile
35
37
  - README_for_rails2.textile
36
38
  - Rakefile
37
39
  - VERSION
40
+ - autotest/discover.rb
38
41
  - bullet.gemspec
39
42
  - lib/bullet.rb
40
43
  - lib/bullet/action_controller2.rb
41
44
  - lib/bullet/active_record2.rb
42
45
  - lib/bullet/active_record3.rb
43
- - lib/bullet/association.rb
44
- - lib/bullet/counter.rb
45
- - lib/bullet/logger.rb
46
+ - lib/bullet/detector.rb
47
+ - lib/bullet/detector/association.rb
48
+ - lib/bullet/detector/base.rb
49
+ - lib/bullet/detector/counter.rb
50
+ - lib/bullet/detector/n_plus_one_query.rb
51
+ - lib/bullet/detector/unused_eager_association.rb
46
52
  - lib/bullet/notification.rb
47
- - lib/bulletware.rb
53
+ - lib/bullet/notification/base.rb
54
+ - lib/bullet/notification/counter_cache.rb
55
+ - lib/bullet/notification/n_plus_one_query.rb
56
+ - lib/bullet/notification/unused_eager_loading.rb
57
+ - lib/bullet/notification_collector.rb
58
+ - lib/bullet/presenter.rb
59
+ - lib/bullet/presenter/base.rb
60
+ - lib/bullet/presenter/bullet_logger.rb
61
+ - lib/bullet/presenter/growl.rb
62
+ - lib/bullet/presenter/javascript_alert.rb
63
+ - lib/bullet/presenter/javascript_console.rb
64
+ - lib/bullet/presenter/javascript_helpers.rb
65
+ - lib/bullet/presenter/rails_logger.rb
66
+ - lib/bullet/presenter/xmpp.rb
67
+ - lib/bullet/rack.rb
68
+ - lib/bullet/registry.rb
69
+ - lib/bullet/registry/association.rb
70
+ - lib/bullet/registry/base.rb
71
+ - lib/bullet/registry/object.rb
48
72
  - rails/init.rb
49
73
  - spec/bullet/association_for_chris_spec.rb
50
74
  - spec/bullet/association_for_peschkaj_spec.rb
51
75
  - spec/bullet/association_spec.rb
52
76
  - spec/bullet/counter_spec.rb
53
- - spec/spec.opts
54
77
  - spec/spec_helper.rb
55
78
  - tasks/bullet_tasks.rake
56
79
  has_rdoc: true
@@ -84,7 +107,7 @@ rubyforge_project:
84
107
  rubygems_version: 1.3.6
85
108
  signing_key:
86
109
  specification_version: 3
87
- summary: A plugin to kill N+1 queries and unused eager loading
110
+ summary: A rails plugin to kill N+1 queries and unused eager loading
88
111
  test_files:
89
112
  - spec/spec_helper.rb
90
113
  - spec/bullet/counter_spec.rb
@@ -1,294 +0,0 @@
1
- module Bullet
2
- class Association
3
- class <<self
4
- include Bullet::Notification
5
-
6
- def start_request
7
- @@checked = false
8
- end
9
-
10
- def end_request
11
- clear
12
- end
13
-
14
- def clear
15
- @@object_associations = nil
16
- @@unpreload_associations = nil
17
- @@unused_preload_associations = nil
18
- @@callers = nil
19
- @@possible_objects = nil
20
- @@impossible_objects = nil
21
- @@call_object_associations = nil
22
- @@eager_loadings = nil
23
- end
24
-
25
- def notification?
26
- check_unused_preload_associations unless @@checked
27
- has_unpreload_associations? or has_unused_preload_associations?
28
- end
29
-
30
- def add_unpreload_associations(klazz, associations)
31
- unpreload_associations[klazz] ||= []
32
- unpreload_associations[klazz] << associations
33
- unique(unpreload_associations[klazz])
34
- end
35
-
36
- def add_unused_preload_associations(klazz, associations)
37
- unused_preload_associations[klazz] ||= []
38
- unused_preload_associations[klazz] << associations
39
- unique(unused_preload_associations[klazz])
40
- end
41
-
42
- def add_object_associations(object, associations)
43
- object_associations[object] ||= []
44
- object_associations[object] << associations
45
- unique(object_associations[object])
46
- end
47
-
48
- def add_call_object_associations(object, associations)
49
- call_object_associations[object] ||= []
50
- call_object_associations[object] << associations
51
- unique(call_object_associations[object])
52
- end
53
-
54
- def add_possible_objects(objects)
55
- klazz = objects.is_a?(Array) ? objects.first.class : objects.class
56
- possible_objects[klazz] ||= []
57
- possible_objects[klazz] << objects
58
- unique(possible_objects[klazz])
59
- end
60
-
61
- def add_impossible_object(object)
62
- klazz = object.class
63
- impossible_objects[klazz] ||= []
64
- impossible_objects[klazz] << object
65
- impossible_objects[klazz].uniq!
66
- end
67
-
68
- def add_eager_loadings(objects, associations)
69
- objects = Array(objects)
70
- eager_loadings[objects] ||= []
71
- eager_loadings.each do |k, v|
72
- unless (k & objects).empty?
73
- if (k & objects) == k
74
- eager_loadings[k] << associations
75
- unique(eager_loadings[k])
76
- break
77
- else
78
- eager_loadings.merge!({(k & objects) => (eager_loadings[k].dup << associations)})
79
- unique(eager_loadings[(k & objects)])
80
- eager_loadings.merge!({(k - objects) => eager_loadings[k]}) unless (k - objects).empty?
81
- unique(eager_loadings[(k - objects)])
82
- eager_loadings.delete(k)
83
- objects = objects - k
84
- end
85
- end
86
- end
87
- unless objects.empty?
88
- eager_loadings[objects] << associations
89
- unique(eager_loadings[objects])
90
- end
91
- end
92
-
93
- # executed when object.assocations is called.
94
- # first, it keeps this method call for object.association.
95
- # then, it checks if this associations call is unpreload.
96
- # if it is, keeps this unpreload associations and caller.
97
- def call_association(object, associations)
98
- add_call_object_associations(object, associations)
99
- if unpreload_associations?(object, associations)
100
- add_unpreload_associations(object.class, associations)
101
- caller_in_project
102
- end
103
- end
104
-
105
- # check if there are unused preload associations.
106
- # for each object => association
107
- # get related_objects from eager_loadings associated with object and associations
108
- # get call_object_association from associations of call_object_associations whose object is in related_objects
109
- # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
110
- def check_unused_preload_associations
111
- @@checked = true
112
- object_associations.each do |object, association|
113
- related_objects = eager_loadings.select {|key, value| key.include?(object) and value == association}.collect(&:first).flatten
114
- call_object_association = related_objects.collect { |related_object| call_object_associations[related_object] }.compact.flatten.uniq
115
- diff_object_association = (association - call_object_association).reject {|a| a.is_a? Hash}
116
- add_unused_preload_associations(object.class, diff_object_association) unless diff_object_association.empty?
117
- end
118
- end
119
-
120
- def has_unused_preload_associations?
121
- !unused_preload_associations.empty?
122
- end
123
-
124
- def has_unpreload_associations?
125
- !unpreload_associations.empty?
126
- end
127
-
128
- private
129
- # decide whether the object.associations is unpreloaded or not.
130
- def unpreload_associations?(object, associations)
131
- possible?(object) and !impossible?(object) and !association?(object, associations)
132
- end
133
-
134
- def possible?(object)
135
- klazz = object.class
136
- possible_objects[klazz] and possible_objects[klazz].include?(object)
137
- end
138
-
139
- def impossible?(object)
140
- klazz = object.class
141
- impossible_objects[klazz] and impossible_objects[klazz].include?(object)
142
- end
143
-
144
- # check if object => associations already exists in object_associations.
145
- def association?(object, associations)
146
- object_associations.each do |key, value|
147
- if key == object
148
- value.each do |v|
149
- result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
150
- return true if result
151
- end
152
- end
153
- end
154
- return false
155
- end
156
-
157
- def notification_response
158
- response = []
159
- if has_unused_preload_associations?
160
- response << unused_preload_messages.join("\n")
161
- end
162
- if has_unpreload_associations?
163
- response << unpreload_messages.join("\n")
164
- end
165
- response
166
- end
167
-
168
- def console_title
169
- title = []
170
- title << unused_preload_messages.first.first unless unused_preload_messages.empty?
171
- title << unpreload_messages.first.first unless unpreload_messages.empty?
172
- title
173
- end
174
-
175
- def log_messages(path = nil)
176
- messages = []
177
- messages << unused_preload_messages(path)
178
- messages << unpreload_messages(path)
179
- messages << call_stack_messages
180
- messages
181
- end
182
-
183
- def unused_preload_messages(path = nil)
184
- messages = []
185
- unused_preload_associations.each do |klazz, associations|
186
- messages << [
187
- "Unused Eager Loading #{path ? "in #{path}" : 'detected'}",
188
- klazz_associations_str(klazz, associations),
189
- " Remove from your finder: #{associations_str(associations)}"
190
- ]
191
- end
192
- messages
193
- end
194
-
195
- def unpreload_messages(path = nil)
196
- messages = []
197
- unpreload_associations.each do |klazz, associations|
198
- messages << [
199
- "N+1 Query #{path ? "in #{path}" : 'detected'}",
200
- klazz_associations_str(klazz, associations),
201
- " Add to your finder: #{associations_str(associations)}"
202
- ]
203
- end
204
- messages
205
- end
206
-
207
- def call_stack_messages
208
- callers.inject([]) do |messages, c|
209
- messages << ['N+1 Query method call stack', c.collect {|line| " #{line}"}].flatten
210
- end
211
- end
212
-
213
- def klazz_associations_str(klazz, associations)
214
- " #{klazz} => [#{associations.map(&:inspect).join(', ')}]"
215
- end
216
-
217
- def associations_str(associations)
218
- ":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
219
- end
220
-
221
- def unique(array)
222
- array.flatten!
223
- array.uniq!
224
- end
225
-
226
- # unpreload_associations keep the class relationships
227
- # that the associations, belongs to the class, are used but not preloaded.
228
- # e.g. { Post => [:comments] }
229
- # so the unpreload_associations should be preloaded by find :include.
230
- def unpreload_associations
231
- @@unpreload_associations ||= {}
232
- end
233
-
234
- # unused_preload_associations keep the class relationships
235
- # that the associations, belongs to the class, are preloaded but not used.
236
- # e.g. { Post => [:comments] }
237
- # so the unused_preload_associations should be removed from find :include.
238
- def unused_preload_associations
239
- @@unused_preload_associations ||= {}
240
- end
241
-
242
- # object_associations keep the object relationships
243
- # that the object has many associations.
244
- # e.g. { <Post id:1> => [:comments] }
245
- # the object_associations keep all associations that may be or may no be
246
- # unpreload associations or unused preload associations.
247
- def object_associations
248
- @@object_associations ||= {}
249
- end
250
-
251
- # call_object_assciations keep the object relationships
252
- # that object.associations is called.
253
- # e.g. { <Post id:1> => [:comments] }
254
- # they are used to detect unused preload associations.
255
- def call_object_associations
256
- @@call_object_associations ||= {}
257
- end
258
-
259
- # possible_objects keep the class to object relationships
260
- # that the objects may cause N+1 query.
261
- # e.g. { Post => [<Post id:1>, <Post id:2>] }
262
- def possible_objects
263
- @@possible_objects ||= {}
264
- end
265
-
266
- # impossible_objects keep the class to objects relationships
267
- # that the objects may not cause N+1 query.
268
- # e.g. { Post => [<Post id:1>, <Post id:2>] }
269
- # Notice: impossible_objects are not accurate,
270
- # if find collection returns only one object, then the object is impossible object,
271
- # impossible_objects are used to avoid treating 1+1 query to N+1 query.
272
- def impossible_objects
273
- @@impossible_objects ||= {}
274
- end
275
-
276
- # eager_loadings keep the object relationships
277
- # that the associations are preloaded by find :include.
278
- # e.g. { [<Post id:1>, <Post id:2>] => [:comments, :user] }
279
- def eager_loadings
280
- @@eager_loadings ||= {}
281
- end
282
-
283
- def caller_in_project
284
- vender_root ||= File.join(Rails.root, 'vendor')
285
- callers << caller.select {|c| c =~ /#{Rails.root}/}.reject {|c| c =~ /#{vender_root}/}
286
- callers.uniq!
287
- end
288
-
289
- def callers
290
- @@callers ||= []
291
- end
292
- end
293
- end
294
- end