├── rails └── init.rb ├── .rspec ├── spec ├── models │ ├── page.rb │ ├── folder.rb │ ├── writer.rb │ ├── person.rb │ ├── author.rb │ ├── city.rb │ ├── company.rb │ ├── country.rb │ ├── entry.rb │ ├── address.rb │ ├── reply.rb │ ├── pet.rb │ ├── student.rb │ ├── teacher.rb │ ├── mongoid │ │ ├── user.rb │ │ ├── comment.rb │ │ ├── company.rb │ │ ├── address.rb │ │ ├── entry.rb │ │ ├── category.rb │ │ └── post.rb │ ├── user.rb │ ├── newspaper.rb │ ├── submission.rb │ ├── relationship.rb │ ├── firm.rb │ ├── base_user.rb │ ├── client.rb │ ├── comment.rb │ ├── document.rb │ ├── category.rb │ └── post.rb ├── bullet │ ├── detector │ │ ├── base_spec.rb │ │ ├── association_spec.rb │ │ ├── counter_cache_spec.rb │ │ ├── unused_eager_loading_spec.rb │ │ └── n_plus_one_query_spec.rb │ ├── notification │ │ ├── counter_cache_spec.rb │ │ ├── unused_eager_loading_spec.rb │ │ ├── n_plus_one_query_spec.rb │ │ └── base_spec.rb │ ├── ext │ │ ├── string_spec.rb │ │ └── object_spec.rb │ ├── registry │ │ ├── object_spec.rb │ │ ├── association_spec.rb │ │ └── base_spec.rb │ ├── notification_collector_spec.rb │ └── rack_spec.rb ├── support │ ├── rack_double.rb │ ├── bullet_ext.rb │ ├── mongo_seed.rb │ └── sqlite_seed.rb ├── bullet_spec.rb ├── integration │ ├── counter_cache_spec.rb │ └── mongoid │ │ └── association_spec.rb └── spec_helper.rb ├── lib ├── bullet │ ├── version.rb │ ├── detector │ │ ├── base.rb │ │ ├── counter_cache.rb │ │ ├── association.rb │ │ ├── unused_eager_loading.rb │ │ └── n_plus_one_query.rb │ ├── ext │ │ ├── string.rb │ │ └── object.rb │ ├── registry.rb │ ├── notification │ │ ├── counter_cache.rb │ │ ├── unused_eager_loading.rb │ │ ├── n_plus_one_query.rb │ │ └── base.rb │ ├── registry │ │ ├── object.rb │ │ ├── association.rb │ │ └── base.rb │ ├── detector.rb │ ├── notification.rb │ ├── notification_collector.rb │ ├── mongoid2x.rb │ ├── mongoid3x.rb │ ├── mongoid4x.rb │ ├── mongoid5x.rb │ ├── dependency.rb │ ├── rack.rb │ ├── active_record4.rb │ ├── active_record41.rb │ ├── active_record3x.rb │ ├── active_record3.rb │ └── active_record42.rb └── bullet.rb ├── .gitignore ├── tasks └── bullet_tasks.rake ├── Gemfile.mongoid ├── Guardfile ├── Gemfile.rails-4.1 ├── Gemfile.rails-4.2 ├── Gemfile.mongoid-2.4 ├── Gemfile.mongoid-2.5 ├── Gemfile.mongoid-2.6 ├── Gemfile.mongoid-2.7 ├── Gemfile.mongoid-2.8 ├── Gemfile.mongoid-3.0 ├── Gemfile.mongoid-3.1 ├── Gemfile.mongoid-4.0 ├── Gemfile.rails-3.0 ├── Gemfile.rails-3.1 ├── Gemfile.rails-3.2 ├── Gemfile.rails-4.0 ├── Gemfile.mongoid-5.0 ├── Gemfile ├── update.sh ├── bullet.gemspec ├── MIT-LICENSE ├── .travis.yml ├── Rakefile ├── test.sh ├── CHANGELOG.md ├── Hacking.md ├── perf └── benchmark.rb └── README.md /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'bullet' 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format progress 3 | -------------------------------------------------------------------------------- /spec/models/page.rb: -------------------------------------------------------------------------------- 1 | class Page < Document 2 | end 3 | -------------------------------------------------------------------------------- /spec/models/folder.rb: -------------------------------------------------------------------------------- 1 | class Folder < Document 2 | end 3 | -------------------------------------------------------------------------------- /spec/models/writer.rb: -------------------------------------------------------------------------------- 1 | class Writer < BaseUser 2 | end 3 | -------------------------------------------------------------------------------- /spec/models/person.rb: -------------------------------------------------------------------------------- 1 | class Person < ActiveRecord::Base 2 | has_many :pets 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/author.rb: -------------------------------------------------------------------------------- 1 | class Author < ActiveRecord::Base 2 | has_many :documents 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/city.rb: -------------------------------------------------------------------------------- 1 | class City < ActiveRecord::Base 2 | belongs_to :country 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | has_one :address 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/country.rb: -------------------------------------------------------------------------------- 1 | class Country < ActiveRecord::Base 2 | has_many :cities 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/entry.rb: -------------------------------------------------------------------------------- 1 | class Entry < ActiveRecord::Base 2 | belongs_to :category 3 | end 4 | -------------------------------------------------------------------------------- /lib/bullet/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Bullet 3 | VERSION = "4.14.8" 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/address.rb: -------------------------------------------------------------------------------- 1 | class Address < ActiveRecord::Base 2 | belongs_to :company 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/reply.rb: -------------------------------------------------------------------------------- 1 | class Reply < ActiveRecord::Base 2 | belongs_to :submission 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/pet.rb: -------------------------------------------------------------------------------- 1 | class Pet < ActiveRecord::Base 2 | belongs_to :person, counter_cache: true 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/student.rb: -------------------------------------------------------------------------------- 1 | class Student < ActiveRecord::Base 2 | has_and_belongs_to_many :teachers 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/teacher.rb: -------------------------------------------------------------------------------- 1 | class Teacher < ActiveRecord::Base 2 | has_and_belongs_to_many :students 3 | end 4 | -------------------------------------------------------------------------------- /lib/bullet/detector/base.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Detector 3 | class Base 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/mongoid/user.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::User 2 | include Mongoid::Document 3 | 4 | field :name 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_one :submission 3 | belongs_to :category 4 | end 5 | -------------------------------------------------------------------------------- /lib/bullet/ext/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def bullet_class_name 3 | self.sub(/:[^:]*?$/, "") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/newspaper.rb: -------------------------------------------------------------------------------- 1 | class Newspaper < ActiveRecord::Base 2 | has_many :writers, class_name: "BaseUser" 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/submission.rb: -------------------------------------------------------------------------------- 1 | class Submission < ActiveRecord::Base 2 | belongs_to :user 3 | has_many :replies 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/relationship.rb: -------------------------------------------------------------------------------- 1 | class Relationship < ActiveRecord::Base 2 | belongs_to :firm 3 | belongs_to :client 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/firm.rb: -------------------------------------------------------------------------------- 1 | class Firm < ActiveRecord::Base 2 | has_many :relationships 3 | has_many :clients, through: :relationships 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/base_user.rb: -------------------------------------------------------------------------------- 1 | class BaseUser < ActiveRecord::Base 2 | has_many :comments 3 | has_many :posts 4 | belongs_to :newspaper 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/client.rb: -------------------------------------------------------------------------------- 1 | class Client < ActiveRecord::Base 2 | has_many :relationships 3 | has_many :firms, through: :relationships 4 | end 5 | -------------------------------------------------------------------------------- /spec/bullet/detector/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Detector 5 | describe Base do 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/mongoid/comment.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::Comment 2 | include Mongoid::Document 3 | 4 | field :name 5 | 6 | belongs_to :post, :class_name => "Mongoid::Post" 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/mongoid/company.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::Company 2 | include Mongoid::Document 3 | 4 | field :name 5 | 6 | has_one :address, :class_name => "Mongoid::Address" 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/mongoid/address.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::Address 2 | include Mongoid::Document 3 | 4 | field :name 5 | 6 | belongs_to :company, :class_name => "Mongoid::Company" 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/mongoid/entry.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::Entry 2 | include Mongoid::Document 3 | 4 | field :name 5 | 6 | belongs_to :category, :class_name => "Mongoid::Category" 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | belongs_to :post, inverse_of: :comments 3 | belongs_to :author, class_name: "BaseUser" 4 | 5 | validates :post, presence: true 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log/** 2 | pkg/** 3 | .DS_Store 4 | lib/.DS_Store 5 | .*.swp 6 | coverage.data 7 | tags 8 | .bundle 9 | *.gem 10 | benchmark_profile* 11 | /nbproject/private/ 12 | coverage/ 13 | .coveralls.yml 14 | Gemfile*.lock 15 | -------------------------------------------------------------------------------- /spec/models/mongoid/category.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::Category 2 | include Mongoid::Document 3 | 4 | field :name 5 | 6 | has_many :posts, :class_name => "Mongoid::Post" 7 | has_many :entries, :class_name => "Mongoid::Entry" 8 | end 9 | -------------------------------------------------------------------------------- /lib/bullet/registry.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Registry 3 | autoload :Base, 'bullet/registry/base' 4 | autoload :Object, 'bullet/registry/object' 5 | autoload :Association, 'bullet/registry/association' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/document.rb: -------------------------------------------------------------------------------- 1 | class Document < ActiveRecord::Base 2 | has_many :children, class_name: "Document", foreign_key: 'parent_id' 3 | belongs_to :parent, class_name: "Document", foreign_key: 'parent_id' 4 | belongs_to :author 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ActiveRecord::Base 2 | has_many :posts, inverse_of: :category 3 | has_many :entries 4 | 5 | has_many :users 6 | 7 | def draft_post 8 | posts.draft.first_or_create 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /tasks/bullet_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :bullet do 2 | namespace :log do 3 | desc "Truncates the bullet log file to zero bytes" 4 | task :clear do 5 | f = File.open("log/bullet.log", "w") 6 | f.close 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bullet/notification/counter_cache.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Notification 3 | class CounterCache < Base 4 | def body 5 | klazz_associations_str 6 | end 7 | 8 | def title 9 | "Need Counter Cache" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.mongoid: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', github: 'mongoid/mongoid' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | -------------------------------------------------------------------------------- /lib/bullet/registry/object.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Registry 3 | class Object < Base 4 | def add(bullet_key) 5 | super(bullet_key.bullet_class_name, bullet_key) 6 | end 7 | 8 | def include?(bullet_key) 9 | super(bullet_key.bullet_class_name, bullet_key) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :version => 2, :all_after_pass => false, :all_on_start => false, :cli => "--color --format nested --fail-fast" do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/mongoid/post.rb: -------------------------------------------------------------------------------- 1 | class Mongoid::Post 2 | include Mongoid::Document 3 | 4 | field :name 5 | 6 | has_many :comments, :class_name => "Mongoid::Comment" 7 | belongs_to :category, :class_name => "Mongoid::Category" 8 | 9 | embeds_many :users, :class_name => "Mongoid::User" 10 | 11 | scope :preload_comments, lambda { includes(:comments) } 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.rails-4.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 4.1.0' 6 | gem 'sqlite3' 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.rails-4.2: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 4.2.0' 6 | gem 'sqlite3' 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /lib/bullet/detector.rb: -------------------------------------------------------------------------------- 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 :UnusedEagerLoading, 'bullet/detector/unused_eager_loading' 7 | autoload :CounterCache, 'bullet/detector/counter_cache' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bullet/notification/unused_eager_loading.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Notification 3 | class UnusedEagerLoading < Base 4 | def body 5 | "#{klazz_associations_str}\n Remove from your finder: #{associations_str}" 6 | end 7 | 8 | def title 9 | "Unused Eager Loading #{@path ? "in #{@path}" : 'detected'}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/bullet/notification/counter_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Notification 5 | describe CounterCache do 6 | subject { CounterCache.new(Post, [:comments, :votes]) } 7 | 8 | it { expect(subject.body).to eq(" Post => [:comments, :votes]") } 9 | it { expect(subject.title).to eq("Need Counter Cache") } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.mongoid-2.4: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 2.4.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-2.5: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 2.5.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-2.6: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 2.6.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-2.7: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 2.7.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-2.8: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 2.8.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-3.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 3.0.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-3.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 3.1.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.mongoid-4.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 4.0.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 4.0.0' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.rails-3.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.0.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.rails-3.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.1.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.rails-3.2: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 3.2.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.rails-4.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 4.0.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /lib/bullet/notification.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Notification 3 | autoload :Base, 'bullet/notification/base' 4 | autoload :UnusedEagerLoading, 'bullet/notification/unused_eager_loading' 5 | autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query' 6 | autoload :CounterCache, 'bullet/notification/counter_cache' 7 | 8 | class UnoptimizedQueryError < StandardError; end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/bullet/ext/string_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe String do 4 | context "bullet_class_name" do 5 | it "should only return class name" do 6 | expect("Post:1".bullet_class_name).to eq("Post") 7 | end 8 | 9 | it "should return class name with namespace" do 10 | expect("Mongoid::Post:1234567890".bullet_class_name).to eq("Mongoid::Post") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.mongoid-5.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rails', '~> 4.0.0' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'mongoid', '~> 5.0.0', github: 'mongoid' 9 | 10 | gem "rspec" 11 | 12 | gem 'coveralls', require: false 13 | 14 | platforms :rbx do 15 | gem 'rubysl', '~> 2.0' 16 | gem 'rubinius-developer_tools' 17 | end 18 | -------------------------------------------------------------------------------- /lib/bullet/registry/association.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Registry 3 | class Association < Base 4 | def merge(base, associations) 5 | @registry.merge!(base => associations) 6 | end 7 | 8 | def similarly_associated(base, associations) 9 | @registry.select { |key, value| key.include?(base) && value == associations }.collect(&:first).flatten 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rails', github: 'rails/rails' 6 | gem 'sqlite3', platforms: [:ruby] 7 | gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] 8 | gem 'activerecord-import' 9 | 10 | gem 'rspec' 11 | gem 'guard' 12 | gem 'guard-rspec' 13 | 14 | gem 'coveralls', require: false 15 | 16 | platforms :rbx do 17 | gem 'rubysl', '~> 2.0' 18 | gem 'rubinius-developer_tools' 19 | end 20 | -------------------------------------------------------------------------------- /lib/bullet/notification_collector.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Bullet 4 | class NotificationCollector 5 | attr_reader :collection 6 | 7 | def initialize 8 | reset 9 | end 10 | 11 | def reset 12 | @collection = Set.new 13 | end 14 | 15 | def add(value) 16 | @collection << value 17 | end 18 | 19 | def notifications_present? 20 | !@collection.empty? 21 | end 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /spec/bullet/notification/unused_eager_loading_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Notification 5 | describe UnusedEagerLoading do 6 | subject { UnusedEagerLoading.new(Post, [:comments, :votes], "path") } 7 | 8 | it { expect(subject.body).to eq(" Post => [:comments, :votes]\n Remove from your finder: :includes => [:comments, :votes]") } 9 | it { expect(subject.title).to eq("Unused Eager Loading in path") } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | belongs_to :category, inverse_of: :posts 3 | belongs_to :writer 4 | has_many :comments, inverse_of: :post 5 | 6 | validates :category, presence: true 7 | 8 | scope :preload_comments, -> { includes(:comments) } 9 | scope :in_category_name, ->(name) { where(['categories.name = ?', name]).includes(:category) } 10 | scope :draft, -> { where(active: false) } 11 | 12 | def link=(*) 13 | comments.new 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/bullet/ext/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | def bullet_key 3 | "#{self.class}:#{self.primary_key_value}" 4 | end 5 | 6 | def primary_key_value 7 | if self.class.respond_to?(:primary_keys) && self.class.primary_keys 8 | self.class.primary_keys.map { |primary_key| self.send primary_key }.join(',') 9 | elsif self.class.respond_to?(:primary_key) && self.class.primary_key 10 | self.send self.class.primary_key 11 | else 12 | self.id 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/bullet/registry/object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Registry 5 | describe Object do 6 | let(:post) { Post.first } 7 | let(:another_post) { Post.last } 8 | subject { Object.new.tap { |object| object.add(post.bullet_key) } } 9 | 10 | context "#include?" do 11 | it "should include the object" do 12 | expect(subject).to be_include(post.bullet_key) 13 | end 14 | end 15 | 16 | context "#add" do 17 | it "should add an object" do 18 | subject.add(another_post.bullet_key) 19 | expect(subject).to be_include(another_post.bullet_key) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle update 2 | BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle update 3 | BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle update 4 | BUNDLE_GEMFILE=Gemfile.rails-3.2 bundle update 5 | BUNDLE_GEMFILE=Gemfile.rails-3.1 bundle update 6 | BUNDLE_GEMFILE=Gemfile.rails-3.0 bundle update 7 | BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle update 8 | BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle update 9 | BUNDLE_GEMFILE=Gemfile.mongoid-3.1 bundle update 10 | BUNDLE_GEMFILE=Gemfile.mongoid-3.0 bundle update 11 | BUNDLE_GEMFILE=Gemfile.mongoid-2.8 bundle update 12 | BUNDLE_GEMFILE=Gemfile.mongoid-2.7 bundle update 13 | BUNDLE_GEMFILE=Gemfile.mongoid-2.6 bundle update 14 | BUNDLE_GEMFILE=Gemfile.mongoid-2.5 bundle update 15 | BUNDLE_GEMFILE=Gemfile.mongoid-2.4 bundle update 16 | -------------------------------------------------------------------------------- /lib/bullet/notification/n_plus_one_query.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Notification 3 | class NPlusOneQuery < Base 4 | def initialize(callers, base_class, associations, path = nil) 5 | super(base_class, associations, path) 6 | 7 | @callers = callers 8 | end 9 | 10 | def body 11 | "#{klazz_associations_str}\n Add to your finder: #{associations_str}" 12 | end 13 | 14 | def title 15 | "N+1 Query #{@path ? "in #{@path}" : 'detected'}" 16 | end 17 | 18 | def notification_data 19 | super.merge( 20 | :backtrace => @callers 21 | ) 22 | end 23 | 24 | protected 25 | def call_stack_messages 26 | (['N+1 Query method call stack'] + @callers).join( "\n " ) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/bullet/registry/association_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Registry 5 | describe Association do 6 | subject { Association.new.tap { |association| association.add(["key1", "key2"], "value") } } 7 | 8 | context "#merge" do 9 | it "should merge key/value" do 10 | subject.merge("key0", "value0") 11 | expect(subject["key0"]).to be_include("value0") 12 | end 13 | end 14 | 15 | context "#similarly_associated" do 16 | it "should return similarly associated keys" do 17 | expect(subject.similarly_associated("key1", Set.new(["value"]))).to eq(["key1", "key2"]) 18 | end 19 | 20 | it "should return empty if key does not exist" do 21 | expect(subject.similarly_associated("key3", Set.new(["value"]))).to be_empty 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/bullet/registry/base.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Registry 3 | class Base 4 | attr_reader :registry 5 | 6 | def initialize 7 | @registry = {} 8 | end 9 | 10 | def [](key) 11 | @registry[key] 12 | end 13 | 14 | def each(&block) 15 | @registry.each(&block) 16 | end 17 | 18 | def delete(base) 19 | @registry.delete(base) 20 | end 21 | 22 | def select(*args, &block) 23 | @registry.select(*args, &block) 24 | end 25 | 26 | def add(key, value) 27 | @registry[key] ||= Set.new 28 | if value.is_a? Array 29 | @registry[key] += value 30 | else 31 | @registry[key] << value 32 | end 33 | end 34 | 35 | def include?(key, value) 36 | !@registry[key].nil? && @registry[key].include?(value) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/bullet/detector/association_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Detector 5 | describe Association do 6 | before :all do 7 | @post1 = Post.first 8 | @post2 = Post.last 9 | end 10 | 11 | context ".add_object_association" do 12 | it "should add object, associations pair" do 13 | Association.add_object_associations(@post1, :associations) 14 | expect(Association.send(:object_associations)).to be_include(@post1.bullet_key, :associations) 15 | end 16 | end 17 | 18 | context ".add_call_object_associations" do 19 | it "should add call object, associations pair" do 20 | Association.add_call_object_associations(@post1, :associations) 21 | expect(Association.send(:call_object_associations)).to be_include(@post1.bullet_key, :associations) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/bullet/notification_collector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | describe NotificationCollector do 5 | subject { NotificationCollector.new.tap { |collector| collector.add("value") } } 6 | 7 | context "#add" do 8 | it "should add a value" do 9 | subject.add("value1") 10 | expect(subject.collection).to be_include("value1") 11 | end 12 | end 13 | 14 | context "#reset" do 15 | it "should reset collector" do 16 | subject.reset 17 | expect(subject.collection).to be_empty 18 | end 19 | end 20 | 21 | context "#notifications_present?" do 22 | it "should be true if collection is not empty" do 23 | expect(subject).to be_notifications_present 24 | end 25 | 26 | it "should be false if collection is empty" do 27 | subject.reset 28 | expect(subject).not_to be_notifications_present 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bullet.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require "bullet/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "bullet" 8 | s.version = Bullet::VERSION 9 | s.platform = Gem::Platform::RUBY 10 | s.authors = ["Richard Huang"] 11 | s.email = ["flyerhzm@gmail.com"] 12 | s.homepage = "http://github.com/flyerhzm/bullet" 13 | s.summary = "help to kill N+1 queries and unused eager loading." 14 | s.description = "help to kill N+1 queries and unused eager loading." 15 | 16 | s.license = 'MIT' 17 | 18 | s.required_rubygems_version = ">= 1.3.6" 19 | 20 | s.add_runtime_dependency "activesupport", ">= 3.0.0" 21 | s.add_runtime_dependency "uniform_notifier", "~> 1.9.0" 22 | 23 | s.files = `git ls-files`.split("\n") 24 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 25 | s.require_paths = ["lib"] 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/bullet/notification/n_plus_one_query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Notification 5 | describe NPlusOneQuery do 6 | subject { NPlusOneQuery.new([["caller1", "caller2"]], Post, [:comments, :votes], "path") } 7 | 8 | it { expect(subject.body_with_caller).to eq(" Post => [:comments, :votes]\n Add to your finder: :includes => [:comments, :votes]\nN+1 Query method call stack\n caller1\n caller2\n") } 9 | it { expect([subject.body_with_caller, subject.body_with_caller]).to eq([ " Post => [:comments, :votes]\n Add to your finder: :includes => [:comments, :votes]\nN+1 Query method call stack\n caller1\n caller2\n", " Post => [:comments, :votes]\n Add to your finder: :includes => [:comments, :votes]\nN+1 Query method call stack\n caller1\n caller2\n" ]) } 10 | it { expect(subject.body).to eq(" Post => [:comments, :votes]\n Add to your finder: :includes => [:comments, :votes]") } 11 | it { expect(subject.title).to eq("N+1 Query in path") } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/rack_double.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | class AppDouble 3 | def call env 4 | env = @env 5 | [ status, headers, response ] 6 | end 7 | 8 | def status= status 9 | @status = status 10 | end 11 | 12 | def headers= headers 13 | @headers = headers 14 | end 15 | 16 | def headers 17 | @headers ||= {"Content-Type" => "text/html"} 18 | @headers 19 | end 20 | 21 | def response= response 22 | @response = response 23 | end 24 | 25 | private 26 | def status 27 | @status || 200 28 | end 29 | 30 | def response 31 | @response || ResponseDouble.new 32 | end 33 | end 34 | 35 | class ResponseDouble 36 | def initialize actual_body = nil 37 | @actual_body = actual_body 38 | end 39 | 40 | def body 41 | @body ||= "
" 42 | end 43 | 44 | def body= body 45 | @body = body 46 | end 47 | 48 | def each 49 | yield body 50 | end 51 | 52 | def close 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0 4 | - 2.1 5 | - 2.2 6 | gemfile: 7 | - Gemfile.rails-4.2 8 | - Gemfile.rails-4.1 9 | - Gemfile.rails-4.0 10 | - Gemfile.rails-3.2 11 | - Gemfile.rails-3.1 12 | - Gemfile.rails-3.0 13 | - Gemfile.mongoid-5.0 14 | - Gemfile.mongoid-4.0 15 | - Gemfile.mongoid-3.1 16 | - Gemfile.mongoid-3.0 17 | - Gemfile.mongoid-2.8 18 | - Gemfile.mongoid-2.7 19 | - Gemfile.mongoid-2.6 20 | - Gemfile.mongoid-2.5 21 | - Gemfile.mongoid-2.4 22 | env: 23 | - DB=sqlite 24 | services: 25 | - mongodb 26 | matrix: 27 | exclude: 28 | - rvm: 2.2 29 | gemfile: Gemfile.rails-3.0 30 | - rvm: 2.2 31 | gemfile: Gemfile.rails-3.1 32 | - rvm: 2.2 33 | gemfile: Gemfile.rails-3.2 34 | - rvm: 2.2 35 | gemfile: Gemfile.mongoid-3.1 36 | - rvm: 2.2 37 | gemfile: Gemfile.mongoid-3.0 38 | - rvm: 2.2 39 | gemfile: Gemfile.mongoid-2.8 40 | - rvm: 2.2 41 | gemfile: Gemfile.mongoid-2.7 42 | - rvm: 2.2 43 | gemfile: Gemfile.mongoid-2.6 44 | - rvm: 2.2 45 | gemfile: Gemfile.mongoid-2.5 46 | - rvm: 2.2 47 | gemfile: Gemfile.mongoid-2.4 48 | -------------------------------------------------------------------------------- /spec/bullet_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Bullet, focused: true do 4 | subject { Bullet } 5 | 6 | describe '#enable' do 7 | 8 | context 'enable Bullet' do 9 | before do 10 | # Bullet.enable 11 | # Do nothing. Bullet has already been enabled for the whole test suite. 12 | end 13 | 14 | it 'should be enabled' do 15 | expect(subject).to be_enable 16 | end 17 | 18 | context 'disable Bullet' do 19 | before do 20 | Bullet.enable = false 21 | end 22 | 23 | it 'should be disabled' do 24 | expect(subject).to_not be_enable 25 | end 26 | 27 | context 'enable Bullet again without patching again the orms' do 28 | before do 29 | expect(Bullet::Mongoid).not_to receive(:enable) if defined? Bullet::Mongoid 30 | expect(Bullet::ActiveRecord).not_to receive(:enable) if defined? Bullet::ActiveRecord 31 | Bullet.enable = true 32 | end 33 | 34 | it 'should be enabled again' do 35 | expect(subject).to be_enable 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/bullet/ext/object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Object do 4 | context "bullet_key" do 5 | it "should return class and id composition" do 6 | post = Post.first 7 | expect(post.bullet_key).to eq("Post:#{post.id}") 8 | end 9 | 10 | if mongoid? 11 | it "should return class with namesapce and id composition" do 12 | post = Mongoid::Post.first 13 | expect(post.bullet_key).to eq("Mongoid::Post:#{post.id}") 14 | end 15 | end 16 | end 17 | 18 | context "primary_key_value" do 19 | it "should return id" do 20 | post = Post.first 21 | expect(post.primary_key_value).to eq(post.id) 22 | end 23 | 24 | it "should return primary key value" do 25 | post = Post.first 26 | Post.primary_key = 'name' 27 | expect(post.primary_key_value).to eq(post.name) 28 | Post.primary_key = 'id' 29 | end 30 | 31 | it "should return value for multiple primary keys" do 32 | post = Post.first 33 | allow(Post).to receive(:primary_keys).and_return([:category_id, :writer_id]) 34 | expect(post.primary_key_value).to eq("#{post.category_id},#{post.writer_id}") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/bullet/registry/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bullet 4 | module Registry 5 | describe Base do 6 | subject { Base.new.tap { |base| base.add("key", "value") } } 7 | 8 | context "#[]" do 9 | it "should get value by key" do 10 | expect(subject["key"]).to eq(Set.new(["value"])) 11 | end 12 | end 13 | 14 | context "#delete" do 15 | it "should delete key" do 16 | subject.delete("key") 17 | expect(subject["key"]).to be_nil 18 | end 19 | end 20 | 21 | context "#add" do 22 | it "should add value with string" do 23 | subject.add("key", "new_value") 24 | expect(subject["key"]).to eq(Set.new(["value", "new_value"])) 25 | end 26 | 27 | it "should add value with array" do 28 | subject.add("key", ["value1", "value2"]) 29 | expect(subject["key"]).to eq(Set.new(["value", "value1", "value2"])) 30 | end 31 | end 32 | 33 | context "#include?" do 34 | it "should include key/value" do 35 | expect(subject.include?("key", "value")).to eq true 36 | end 37 | 38 | it "should not include wrong key/value" do 39 | expect(subject.include?("key", "val")).to eq false 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 2 | require "bundler" 3 | Bundler.setup 4 | 5 | require "rake" 6 | require "rspec" 7 | require "rspec/core/rake_task" 8 | 9 | require "bullet/version" 10 | 11 | task :build do 12 | system "gem build bullet.gemspec" 13 | end 14 | 15 | task :install => :build do 16 | system "sudo gem install bullet-#{Bullet::VERSION}.gem" 17 | end 18 | 19 | task :release => :build do 20 | puts "Tagging #{Bullet::VERSION}..." 21 | system "git tag -a #{Bullet::VERSION} -m 'Tagging #{Bullet::VERSION}'" 22 | puts "Pushing to Github..." 23 | system "git push --tags" 24 | puts "Pushing to rubygems.org..." 25 | system "gem push bullet-#{Bullet::VERSION}.gem" 26 | end 27 | 28 | RSpec::Core::RakeTask.new(:spec) do |spec| 29 | spec.pattern = "spec/**/*_spec.rb" 30 | end 31 | 32 | RSpec::Core::RakeTask.new('spec:progress') do |spec| 33 | spec.rspec_opts = %w(--format progress) 34 | spec.pattern = "spec/**/*_spec.rb" 35 | end 36 | 37 | 38 | begin 39 | require 'rdoc/task' 40 | 41 | desc "Generate documentation for the plugin." 42 | Rake::RDocTask.new do |rdoc| 43 | rdoc.rdoc_dir = "rdoc" 44 | rdoc.title = "bullet #{Bullet::VERSION}" 45 | rdoc.rdoc_files.include("README*") 46 | rdoc.rdoc_files.include("lib/**/*.rb") 47 | end 48 | rescue LoadError 49 | puts 'RDocTask is not supported for this platform' 50 | end 51 | 52 | task :default => :spec 53 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #bundle update rails && bundle exec rspec spec 2 | #BUNDLE_GEMFILE=Gemfile.mongoid bundle update mongoid && BUNDLE_GEMFILE=Gemfile.mongoid bundle exec rspec spec 3 | BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle && BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle exec rspec spec 4 | BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle exec rspec spec 5 | BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle exec rspec spec 6 | BUNDLE_GEMFILE=Gemfile.rails-3.2 bundle && BUNDLE_GEMFILE=Gemfile.rails-3.2 bundle exec rspec spec 7 | BUNDLE_GEMFILE=Gemfile.rails-3.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-3.1 bundle exec rspec spec 8 | BUNDLE_GEMFILE=Gemfile.rails-3.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-3.0 bundle exec rspec spec 9 | BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle exec rspec spec 10 | BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle exec rspec spec 11 | BUNDLE_GEMFILE=Gemfile.mongoid-3.1 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-3.1 bundle exec rspec spec 12 | BUNDLE_GEMFILE=Gemfile.mongoid-3.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-3.0 bundle exec rspec spec 13 | BUNDLE_GEMFILE=Gemfile.mongoid-2.8 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-2.8 bundle exec rspec spec 14 | BUNDLE_GEMFILE=Gemfile.mongoid-2.7 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-2.7 bundle exec rspec spec 15 | BUNDLE_GEMFILE=Gemfile.mongoid-2.6 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-2.6 bundle exec rspec spec 16 | BUNDLE_GEMFILE=Gemfile.mongoid-2.5 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-2.5 bundle exec rspec spec 17 | BUNDLE_GEMFILE=Gemfile.mongoid-2.4 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-2.4 bundle exec rspec spec 18 | -------------------------------------------------------------------------------- /lib/bullet/mongoid2x.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Mongoid 3 | def self.enable 4 | require 'mongoid' 5 | 6 | ::Mongoid::Contexts::Mongo.class_eval do 7 | alias_method :origin_first, :first 8 | alias_method :origin_last, :last 9 | alias_method :origin_iterate, :iterate 10 | alias_method :origin_eager_load, :eager_load 11 | 12 | def first 13 | result = origin_first 14 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 15 | result 16 | end 17 | 18 | def last 19 | result = origin_last 20 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 21 | result 22 | end 23 | 24 | def iterate(&block) 25 | records = execute.to_a 26 | if records.size > 1 27 | Bullet::Detector::NPlusOneQuery.add_possible_objects(records) 28 | elsif records.size == 1 29 | Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) 30 | end 31 | origin_iterate(&block) 32 | end 33 | 34 | def eager_load(docs) 35 | associations = criteria.inclusions.map(&:name) 36 | docs.each do |doc| 37 | Bullet::Detector::Association.add_object_associations(doc, associations) 38 | end 39 | Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) 40 | origin_eager_load(docs) 41 | end 42 | end 43 | 44 | ::Mongoid::Relations::Accessors.class_eval do 45 | alias_method :origin_set_relation, :set_relation 46 | 47 | def set_relation(name, relation) 48 | if relation && relation.metadata.macro !~ /embed/ 49 | Bullet::Detector::NPlusOneQuery.call_association(self, name) 50 | end 51 | origin_set_relation(name, relation) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/bullet/mongoid3x.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Mongoid 3 | def self.enable 4 | require 'mongoid' 5 | ::Mongoid::Contextual::Mongo.class_eval do 6 | alias_method :origin_first, :first 7 | alias_method :origin_last, :last 8 | alias_method :origin_each, :each 9 | alias_method :origin_eager_load, :eager_load 10 | 11 | def first 12 | result = origin_first 13 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 14 | result 15 | end 16 | 17 | def last 18 | result = origin_last 19 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 20 | result 21 | end 22 | 23 | def each(&block) 24 | records = query.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) } 25 | if records.length > 1 26 | Bullet::Detector::NPlusOneQuery.add_possible_objects(records) 27 | elsif records.size == 1 28 | Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) 29 | end 30 | origin_each(&block) 31 | end 32 | 33 | def eager_load(docs) 34 | associations = criteria.inclusions.map(&:name) 35 | docs.each do |doc| 36 | Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) 37 | end 38 | Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) 39 | origin_eager_load(docs) 40 | end 41 | end 42 | 43 | ::Mongoid::Relations::Accessors.class_eval do 44 | alias_method :origin_get_relation, :get_relation 45 | 46 | def get_relation(name, metadata, reload = false) 47 | result = origin_get_relation(name, metadata, reload) 48 | if metadata.macro !~ /embed/ 49 | Bullet::Detector::NPlusOneQuery.call_association(self, name) 50 | end 51 | result 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/bullet/mongoid4x.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Mongoid 3 | def self.enable 4 | require 'mongoid' 5 | ::Mongoid::Contextual::Mongo.class_eval do 6 | alias_method :origin_first, :first 7 | alias_method :origin_last, :last 8 | alias_method :origin_each, :each 9 | alias_method :origin_eager_load, :eager_load 10 | 11 | def first 12 | result = origin_first 13 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 14 | result 15 | end 16 | 17 | def last 18 | result = origin_last 19 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 20 | result 21 | end 22 | 23 | def each(&block) 24 | records = query.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) } 25 | if records.length > 1 26 | Bullet::Detector::NPlusOneQuery.add_possible_objects(records) 27 | elsif records.size == 1 28 | Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) 29 | end 30 | origin_each(&block) 31 | end 32 | 33 | def eager_load(docs) 34 | associations = criteria.inclusions.map(&:name) 35 | docs.each do |doc| 36 | Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) 37 | end 38 | Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) 39 | origin_eager_load(docs) 40 | end 41 | end 42 | 43 | ::Mongoid::Relations::Accessors.class_eval do 44 | alias_method :origin_get_relation, :get_relation 45 | 46 | def get_relation(name, metadata, object, reload = false) 47 | result = origin_get_relation(name, metadata, object, reload) 48 | if metadata.macro !~ /embed/ 49 | Bullet::Detector::NPlusOneQuery.call_association(self, name) 50 | end 51 | result 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/bullet/mongoid5x.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Mongoid 3 | def self.enable 4 | require 'mongoid' 5 | ::Mongoid::Contextual::Mongo.class_eval do 6 | alias_method :origin_first, :first 7 | alias_method :origin_last, :last 8 | alias_method :origin_each, :each 9 | alias_method :origin_eager_load, :eager_load 10 | 11 | def first 12 | result = origin_first 13 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 14 | result 15 | end 16 | 17 | def last 18 | result = origin_last 19 | Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result 20 | result 21 | end 22 | 23 | def each(&block) 24 | records = view.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) } 25 | if records.length > 1 26 | Bullet::Detector::NPlusOneQuery.add_possible_objects(records) 27 | elsif records.size == 1 28 | Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) 29 | end 30 | origin_each(&block) 31 | end 32 | 33 | def eager_load(docs) 34 | associations = criteria.inclusions.map(&:name) 35 | docs.each do |doc| 36 | Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) 37 | end 38 | Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) 39 | origin_eager_load(docs) 40 | end 41 | end 42 | 43 | ::Mongoid::Relations::Accessors.class_eval do 44 | alias_method :origin_get_relation, :get_relation 45 | 46 | def get_relation(name, metadata, object, reload = false) 47 | result = origin_get_relation(name, metadata, object, reload) 48 | if metadata.macro !~ /embed/ 49 | Bullet::Detector::NPlusOneQuery.call_association(self, name) 50 | end 51 | result 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/integration/counter_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if !mongoid? && active_record? 4 | describe Bullet::Detector::CounterCache do 5 | before(:each) do 6 | Bullet.start_request 7 | end 8 | 9 | after(:each) do 10 | Bullet.end_request 11 | end 12 | 13 | it "should need counter cache with all cities" do 14 | Country.all.each do |country| 15 | country.cities.size 16 | end 17 | expect(Bullet.collected_counter_cache_notifications).not_to be_empty 18 | end 19 | 20 | it "should not need counter cache if already define counter_cache" do 21 | Person.all.each do |person| 22 | person.pets.size 23 | end 24 | expect(Bullet.collected_counter_cache_notifications).to be_empty 25 | end 26 | 27 | it "should not need counter cache with only one object" do 28 | Country.first.cities.size 29 | expect(Bullet.collected_counter_cache_notifications).to be_empty 30 | end 31 | 32 | it "should not need counter cache with part of cities" do 33 | Country.all.each do |country| 34 | country.cities.where(:name => 'first').size 35 | end 36 | expect(Bullet.collected_counter_cache_notifications).to be_empty 37 | end 38 | 39 | context "disable" do 40 | before { Bullet.counter_cache_enable = false } 41 | after { Bullet.counter_cache_enable = true } 42 | 43 | it "should not detect counter cache" do 44 | Country.all.each do |country| 45 | country.cities.size 46 | end 47 | expect(Bullet.collected_counter_cache_notifications).to be_empty 48 | end 49 | end 50 | 51 | context "whitelist" do 52 | before { Bullet.add_whitelist :type => :counter_cache, :class_name => "Country", :association => :cities } 53 | after { Bullet.reset_whitelist } 54 | 55 | it "should not detect counter cache" do 56 | Country.all.each do |country| 57 | country.cities.size 58 | end 59 | expect(Bullet.collected_counter_cache_notifications).to be_empty 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/bullet/notification/base.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | module Notification 3 | class Base 4 | attr_accessor :notifier, :url 5 | attr_reader :base_class, :associations, :path 6 | 7 | def initialize(base_class, association_or_associations, path = nil) 8 | @base_class = base_class 9 | @associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations] 10 | @path = path 11 | end 12 | 13 | def title 14 | raise NoMethodError.new("no method title defined") 15 | end 16 | 17 | def body 18 | raise NoMethodError.new("no method body defined") 19 | end 20 | 21 | def call_stack_messages 22 | "" 23 | end 24 | 25 | def whoami 26 | @user ||= ENV['USER'].presence || (`whoami`.chomp rescue "") 27 | if @user.present? 28 | "user: #{@user}" 29 | else 30 | "" 31 | end 32 | end 33 | 34 | def body_with_caller 35 | "#{body}\n#{call_stack_messages}\n" 36 | end 37 | 38 | def notify_inline 39 | self.notifier.inline_notify(notification_data) 40 | end 41 | 42 | def notify_out_of_channel 43 | self.notifier.out_of_channel_notify(notification_data) 44 | end 45 | 46 | def short_notice 47 | [whoami.presence, url, title, body].compact.join(" ") 48 | end 49 | 50 | def notification_data 51 | { 52 | :user => whoami, 53 | :url => url, 54 | :title => title, 55 | :body => body_with_caller, 56 | } 57 | end 58 | 59 | def eql?(other) 60 | klazz_associations_str == other.klazz_associations_str 61 | end 62 | 63 | def hash 64 | klazz_associations_str.hash 65 | end 66 | 67 | protected 68 | def klazz_associations_str 69 | " #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]" 70 | end 71 | 72 | def associations_str 73 | ":includes => #{@associations.map{ |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}" 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/support/bullet_ext.rb: -------------------------------------------------------------------------------- 1 | module Bullet 2 | def self.collected_notifications_of_class(notification_class) 3 | Bullet.notification_collector.collection.select do |notification| 4 | notification.is_a? notification_class 5 | end 6 | end 7 | 8 | def self.collected_counter_cache_notifications 9 | collected_notifications_of_class Bullet::Notification::CounterCache 10 | end 11 | 12 | def self.collected_n_plus_one_query_notifications 13 | collected_notifications_of_class Bullet::Notification::NPlusOneQuery 14 | end 15 | 16 | def self.collected_unused_eager_association_notifications 17 | collected_notifications_of_class Bullet::Notification::UnusedEagerLoading 18 | end 19 | end 20 | 21 | module Bullet 22 | module Detector 23 | class Association 24 | class <