pax_global_header00006660000000000000000000000064145256522530014523gustar00rootroot0000000000000052 comment=5de18679c269931622b44892d68cbd7ecdad0aa6 bullet-7.1.4/000077500000000000000000000000001452565225300130235ustar00rootroot00000000000000bullet-7.1.4/.github/000077500000000000000000000000001452565225300143635ustar00rootroot00000000000000bullet-7.1.4/.github/dependabot.yml000066400000000000000000000001661452565225300172160ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" bullet-7.1.4/.github/workflows/000077500000000000000000000000001452565225300164205ustar00rootroot00000000000000bullet-7.1.4/.github/workflows/main.yml000066400000000000000000000047721452565225300201010ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test_rails_4: runs-on: ubuntu-latest strategy: matrix: gemfile: ['Gemfile.rails-4.0', 'Gemfile.rails-4.1', 'Gemfile.rails-4.2'] env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.3 bundler: 1 bundler-cache: true - name: Run tests run: bundle exec rake test_rails_5: runs-on: ubuntu-latest strategy: matrix: gemfile: ['Gemfile.rails-5.0', 'Gemfile.rails-5.1', 'Gemfile.rails-5.2'] env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.5 bundler: 1 bundler-cache: true - name: Run tests run: bundle exec rake test_rails_6: runs-on: ubuntu-latest strategy: matrix: gemfile: ['Gemfile.rails-6.0', 'Gemfile.rails-6.1'] env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 bundler-cache: true - name: Run tests run: bundle exec rake test_rails_7: runs-on: ubuntu-latest strategy: matrix: gemfile: ['Gemfile.rails-7.0', 'Gemfile.rails-7.1'] env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.1 bundler-cache: true - name: Run tests run: bundle exec rake bullet-7.1.4/.gitignore000066400000000000000000000002431452565225300150120ustar00rootroot00000000000000log/** pkg/** .DS_Store lib/.DS_Store .*.swp coverage.data tags .bundle *.gem benchmark_profile* /nbproject/private/ coverage/ .coveralls.yml Gemfile*.lock .idea/ bullet-7.1.4/.rspec000066400000000000000000000000331452565225300141340ustar00rootroot00000000000000--colour --format progress bullet-7.1.4/CHANGELOG.md000066400000000000000000000171271452565225300146440ustar00rootroot00000000000000## Next Release ## 7.1.4 (11/17/2023) * Call association also on through reflection ## 7.1.3 (11/05/2023) * Call NPlusOneQuery's call_association when calling count on collection assocation ## 7.1.2 (10/13/2023) * Handle Rails 7.1 composite primary keys ## 7.1.1 (10/07/2023) * Add support for `Content-Security-Policy-Report-Only` nonces * Fix count method signature ## 7.1.0 (10/06/2023) * Support rails 7.1 * Alias `Bullet.enable?` to `enabled?`, and `Bullet.enable=` to `enabled=` * Added `always_append_html_body` option, so the html snippet is always included even if there are no notifications * Added detection of n+1 count queries from `count` method * Changed the counter cache notification title to recommend using `size` ## 7.0.7 (03/01/2023) * Check `Rails.application.config.content_security_policy` before insert `Bullet::Rack` ## 7.0.6 (03/01/2023) * Better way to check if `ActionDispatch::ContentSecurityPolicy::Middleware` exists ## 7.0.5 (01/01/2023) * Fix n+1 false positives in AR 7.0 * Fix eager_load nested has_many :through false positives * Respect Content-Security-Policy nonces * Added CallStacks support for avoid eager loading * Iterate fewer times over objects ## 7.0.4 (11/28/2022) * Fix `eager_load` `has_many :through` false positives * mongoid7x: add dynamic methods ## 7.0.3 (08/13/2022) * Replace `Array()` with `Array.wrap()` ## 7.0.2 (05/31/2022) * Drop growl support * Do not check html tag in Bullet::Rack anymore ## 7.0.1 (01/15/2022) * Get rid of *_whitelist methods * Hack ActiveRecord::Associations::Preloader::Batch in rails 7 ## 7.0.0 (12/18/2021) * Support rails 7 * Fix Mongoid 7 view iteration * Move CI from Travis to Github Actions ## 6.1.5 (08/16/2021) * Rename whitelist to safelist * Fix onload called twice * Support Rack::Files::Iterator responses * Ensure HABTM associations are not incorrectly labeled n+1 ## 6.1.4 (02/26/2021) * Added an option to stop adding HTTP headers to API requests ## 6.1.3 (01/21/2021) * Consider ThroughAssociation at SingularAssociation like CollectionAssociation * Add xhr_script only when add_footer is enabled ## 6.1.2 (12/12/2020) * Revert "Make whitelist thread safe" ## 6.1.1 (12/12/2020) * Add support Rails 6.1 * Make whitelist thread safe ## 6.1.0 (12/28/2019) * Add skip_html_injection flag * Remove writer hack in active_record6 * Use modern includes syntax in warnings * Fix warning: The last argument is used as the keyword parameter ## 6.0.2 (08/20/2019) * Fully support Rails 6.0 ## 6.0.1 (06/26/2019) * Add Bullet::ActiveJob * Prevent "Maximum call stack exceeded" errors when used with Turbolinks ## 6.0.0 (04/25/2019) * Add XHR support to Bullet * Support Rails 6.0 * Handle case where ID is manually set on unpersisted record ## 5.9.0 (11/11/2018) * Require Ruby 2.3+ * Support Mongo 7.x ## 5.8.0 (10/29/2018) * Fix through reflection for rails 5.x * Fix false positive in after_save/after_create callbacks * Don't trigger a preload error on "manual" preloads * Avoid Bullet from making extra queries in mongoid6 * Support option for #first and #last on mongoid6.x * Fix duplicate logs in mongoid 4.x and 5.x version * Use caller for ruby 1.9 while caller_locations for 2.0+ * Extend stacktrace matching for sub-file precision * Exclude configured bundler path in addition to '/vendor' * Fix `caller_path` in `excluded_stacktrace_path` * Update `uniform_notifier` dependency to add Sentry support * Integrate awesomecode.io and refactor code ## 5.7.0 (12/03/2017) * Support rails 5.2 * Implement Bullet.delete_whitelist to delete a specific whitelist definition * Fix caller_path in the case of nil ## 5.6.0 (07/16/2017) * Migrate alias_method to Module#prepend * Add install generator * Stack trace filter * Fix rails 5.1 compatibility * Fix inverse_of for rails 5 * Fix detect file attachment for rack #319 ## 5.5.0 (12/30/2016) * Display http request method #311 * Add close button to footer * Raise an error if bullet does not support AR or Mongoid * Avoid double backtrace * Fix false alert on counter cache when associations are already loaded #288 * Fix "false alert" in rails 5 #239 * Do not support ActiveRecord 3.x and Mongoid 3.x / 4.x anymore ## 5.4.0 (10/09/2016) * Support rails 5.1 * Extract stack trace filtering into module ## 5.3.0 (15/08/2016) * Fix false alert on through association with join sql #301 * Fix association.target in `through_association` can be singular #302 * Support `find_by_sql` #303 * Fix env `REQUEST_URI` ## 5.2.0 (07/26/2016) * Fix `has_cached_counter?` is not defined in HABTM #297 * Fix false alert if preloaded association has no records #260 * Support Rails 5.0.0 ## 5.1.0 (05/21/2016) * Fix false alert when `empty?` used with `counter_cache` * Fix `alias_method_chain` deprecation for rails 5 * Add response handling for non-Rails Rack responses * Fix false alert when querying immediately after creation * Fix UnusedEagerLoading bug when multiple eager loading query include same objects ## 5.0.0 (01/06/2016) * Support Rails 5.0.0.beta1 * Fix `has_many :through` infinite loop issue * Support mongoid 5.0.0 * Do not report association queries immediately after object creation to require a preload * Detect `counter_cache` for `has_many :through` association * Compatible with `composite_primary_keys` gem * Fix AR 4.2 SingularAssociation#reader result can be nil * `perform_out_of_channel_notifications` should always be triggered * Fix false positive with `belongs_to` -> `belongs_to` for active\_record 4.2 * Activate active\_record hacks only when Bullet already start * Don't execute query when running `to_sql` * Send backtrace to `uniform_notifier` * Fix sse response check * Dynamically delegate available notifiers to UniformNotifier * Hotfix nil object when `add_impossible_object` * Fix `has_one` then `has_many` associations in rails 4.2 * Append js and dom to html body in proper position ## 4.14.0 (10/03/2014) * Support rails 4.2 * Polish notification output * Fix warning: `*' interpreted as argument prefix ## 4.13.0 (07/19/2014) * Support include? call on ar associations ## 4.12.0 (07/13/2014) * Fix false n+1 queries caused by inversed objects. * Replace .id with .primary_key_value * Rename bullet_ar_key to bullet_key * Fix rails sse detect * Fix bullet using in test environment * Memoize whoami ## 4.11.0 (06/24/2014) * Support empty? call on ar associations * Skip detecting if object is a new record ## 4.10.0 (06/06/2014) * Handle join query smarter * Support mongoid 4.0 * Thread safe * Add debug mode ## 4.9.0 (04/30/2014) * Add Bullet.stacktrace_includes option * Applied keyword argument fixes on Ruby 2.2.0 * Add bugsnag notifier * Support rails 4.1.0 ## 4.8.0 (02/16/2014) * Support rails 4.1.0.beta1 * Update specs to be RSpec 3.0 compatible * Update latest minor version activerecord and mongoid on travis ## 4.7.0 (11/03/2013) * Add coverall support * Add helper to profile code outside a request * Add activesupport dependency * Add Bullet.raise notification * Add Bullet.add_footer notification * Fix activerecord4 warnings in test code ## 4.6.0 (04/18/2013) * Fix Bullet::Rack to support sinatra ## 4.5.0 (03/24/2013) * Add api way to access captured associatioin * Allow disable n_plus_one_query, unused_eager_loading and counter_cache respectively * Add whitelist ## 4.4.0 (03/15/2013) * Remove disable_browser_cache option * Compatible with Rails 4.0.0.beta1 ## 4.3.0 (12/28/2012) * Fix content-length for non ascii html * Add mongoid 2.5.x support ## 4.2.0 (09/29/2012) * Add Bullet::Dependency to check AR and mongoid version * Add Rails 4 support * Add airbrake notifier support ## 4.1.0 (05/30/2012) * Add mongoid 3 support ## 4.0.0 (05/09/2012) * Add mongoid support bullet-7.1.4/Gemfile000066400000000000000000000007631452565225300143240ustar00rootroot00000000000000source 'https://rubygems.org' git_source(:github) do |repo_name| repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/') "https://github.com/#{repo_name}.git" end gemspec gem 'rails', github: 'rails' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem 'rspec' gem 'guard' gem 'guard-rspec' gem 'coveralls', require: false platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.mongoid000066400000000000000000000003511452565225300157500ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'mongoid', github: 'mongoid/mongoid' gem "rspec" gem 'coveralls', require: false bullet-7.1.4/Gemfile.mongoid-4.0000066400000000000000000000004261452565225300162520ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 4.0.0' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'mongoid', '~> 4.0.0' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.mongoid-5.0000066400000000000000000000004261452565225300162530ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 4.0.0' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'mongoid', '~> 5.1.0' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.mongoid-6.0000066400000000000000000000004261452565225300162540ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 5.0.0' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'mongoid', '~> 6.0.0' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.mongoid-7.0000066400000000000000000000004241452565225300162530ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 5.0' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'mongoid', '~> 7.0.0' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.mongoid-8.0000066400000000000000000000004221452565225300162520ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 6.1' gem 'sqlite3', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'mongoid', '~> 8.0' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-4.0000066400000000000000000000005201452565225300157230ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 4.0.0' gem 'sqlite3', '~> 1.3.6', platforms: [:ruby] gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem 'tins', '~> 1.6.0', platforms: [:ruby_19] gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-4.1000066400000000000000000000004741452565225300157340ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 4.1.0' gem 'sqlite3', '~> 1.3.6' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem 'tins', '~> 1.6.0', platforms: [:ruby_19] gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-4.2000066400000000000000000000004741452565225300157350ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 4.2.0' gem 'sqlite3', '~> 1.3.6' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem 'tins', '~> 1.6.0', platforms: [:ruby_19] gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-5.0000066400000000000000000000004161452565225300157300ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 5.0.0' gem 'sqlite3', '~> 1.3.6' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-5.1000066400000000000000000000004161452565225300157310ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 5.1.0' gem 'sqlite3', '~> 1.3.6' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-5.2000066400000000000000000000004161452565225300157320ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 5.2.0' gem 'sqlite3', '~> 1.3.6' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-6.0000066400000000000000000000004021452565225300157240ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 6.0.0' gem 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-6.1000066400000000000000000000004021452565225300157250ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 6.1.0' gem 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" platforms :rbx do gem 'rubysl', '~> 2.0' gem 'rubinius-developer_tools' end bullet-7.1.4/Gemfile.rails-7.0000066400000000000000000000002611452565225300157300ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 7.0.0' gem 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" bullet-7.1.4/Gemfile.rails-7.1000066400000000000000000000002611452565225300157310ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem 'rails', '~> 7.1.0' gem 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' gem "rspec" bullet-7.1.4/Guardfile000066400000000000000000000005161452565225300146520ustar00rootroot00000000000000# A sample Guardfile # More info at https://github.com/guard/guard#readme guard 'rspec', version: 2, all_after_pass: false, all_on_start: false, cli: '--color --format nested --fail-fast' do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { 'spec' } end bullet-7.1.4/Hacking.md000066400000000000000000000065101452565225300147130ustar00rootroot00000000000000# Bullet Overview for Developers This file aims to give developers a quick tour of the bullet internals, making it (hopefully) easier to extend or enhance the Bullet gem. ## General Control Flow aka. 10000 Meter View When Rails is initialized, Bullet will extend ActiveRecord (and if you're using Rails 2.x ActiveController too) with the relevant modules and methods found in lib/bullet/active_recordX.rb and lib/bullet/action_controller2.rb. If you're running Rails 3, Bullet will integrate itself as a middleware into the Rack stack, so ActionController does not need to be extended. The ActiveRecord extensions will call methods in a given detector class, when certain methods are called. Detector classes contain all the logic to recognize a noteworthy event. If such an event is detected, an instance of the corresponding Notification class is created and stored in a Set instance in the main Bullet module (the 'notification collector'). Notification instances contain the message that will be displayed, and will use a Presenter class to display their message to the user. So the flow of a request goes like this: 1. Bullet.start_request is called, which resets all the detectors and empties the notification collector 2. The request is handled by Rails, and the installed ActiveRecord extensions trigger Detector callbacks 3. Detectors once called, will determine whether something noteworthy happened. If yes, then a Notification is created and stored in the notification collector. 4. Rails finishes handling the request 5. For each notification in the collector, Bullet will iterate over each Presenter and will try to generate an inline message that will be appended to the generated response body. 6. The response is returned to the client. 7. Bullet will try to generate an out-of-channel message for each notification. 8. Bullet calls end_request for each detector. 9. Goto 1. ## Adding Notification Types If you want to add more kinds of things that Bullet can detect, a little more work is needed than if you were just adding a Presenter, but the concepts are similar. * Add the class to the DETECTORS constant in the main Bullet module * Add (if needed) Rails monkey patches to Bullet.enable * Add an autoload directive to lib/bullet/detector.rb * Create a corresponding notification class in the Bullet::Notification namespace * Add an autoload directive to lib/bullet/notification.rb As a rule of thumb, you can assume that each Detector will have its own Notification class. If you follow the principle of Separation of Concerns I can't really think of an example where one would deviate from this rule. Since the detection of pathological associations is a bit hairy, I'd recommend having a look at the counter cache detector and associated notification to get a feel for what is needed to get off the ground. ### Detectors The only things you'll need to consider when building your Detector class is that it will need to supply the .start_request, .end_request and .clear class methods. Simple implementations are provided by Bullet::Detector::Base for start_request and end_request, you will have to supply your own clear method. ### Notifications For notifications you will want to supply a #title and #body instance method, and check to see if the #initialize and #full_notice methods in the Bullet::Notification::Base class fit your needs. bullet-7.1.4/MIT-LICENSE000066400000000000000000000020751452565225300144630ustar00rootroot00000000000000Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bullet-7.1.4/README.md000066400000000000000000000362661452565225300143170ustar00rootroot00000000000000# Bullet ![Main workflow](https://github.com/flyerhzm/bullet/actions/workflows/main.yml/badge.svg) [![Gem Version](https://badge.fury.io/rb/bullet.svg)](http://badge.fury.io/rb/bullet) [![AwesomeCode Status for flyerhzm/bullet](https://awesomecode.io/projects/6755235b-e2c1-459e-bf92-b8b13d0c0472/status)](https://awesomecode.io/repos/flyerhzm/bullet) [![Coderwall Endorse](http://api.coderwall.com/flyerhzm/endorsecount.png)](http://coderwall.com/flyerhzm) The Bullet gem is designed to help you increase your application's performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries), when you're using eager loading that isn't necessary and when you should use counter cache. Best practice is to use Bullet in development mode or custom mode (staging, profile, etc.). The last thing you want is your clients getting alerts about how lazy you are. Bullet gem now supports **activerecord** >= 4.0 and **mongoid** >= 4.0. If you use activerecord 2.x, please use bullet <= 4.5.0 If you use activerecord 3.x, please use bullet < 5.5.0 ## External Introduction * [http://railscasts.com/episodes/372-bullet](http://railscasts.com/episodes/372-bullet) * [http://ruby5.envylabs.com/episodes/9-episode-8-september-8-2009](http://ruby5.envylabs.com/episodes/9-episode-8-september-8-2009) * [http://railslab.newrelic.com/2009/10/23/episode-19-on-the-edge-part-1](http://railslab.newrelic.com/2009/10/23/episode-19-on-the-edge-part-1) * [http://weblog.rubyonrails.org/2009/10/22/community-highlights](http://weblog.rubyonrails.org/2009/10/22/community-highlights) ## Install You can install it as a gem: ``` gem install bullet ``` or add it into a Gemfile (Bundler): ```ruby gem 'bullet', group: 'development' ``` enable the Bullet gem with generate command ```ruby bundle exec rails g bullet:install ``` The generate command will auto generate the default configuration and may ask to include in the test environment as well. See below for custom configuration. **Note**: make sure `bullet` gem is added after activerecord (rails) and mongoid. ## Configuration Bullet won't enable any notification systems unless you tell it to explicitly. Append to `config/environments/development.rb` initializer with the following code: ```ruby config.after_initialize do Bullet.enable = true Bullet.sentry = true Bullet.alert = true Bullet.bullet_logger = true Bullet.console = true Bullet.xmpp = { :account => 'bullets_account@jabber.org', :password => 'bullets_password_for_jabber', :receiver => 'your_account@jabber.org', :show_online_status => true } Bullet.rails_logger = true Bullet.honeybadger = true Bullet.bugsnag = true Bullet.appsignal = true Bullet.airbrake = true Bullet.rollbar = true Bullet.add_footer = true Bullet.skip_html_injection = false Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ] Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ] Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' } end ``` The notifier of Bullet is a wrap of [uniform_notifier](https://github.com/flyerhzm/uniform_notifier) The code above will enable all of the Bullet notification systems: * `Bullet.enable`: enable Bullet gem, otherwise do nothing * `Bullet.alert`: pop up a JavaScript alert in the browser * `Bullet.bullet_logger`: log to the Bullet log file (Rails.root/log/bullet.log) * `Bullet.console`: log warnings to your browser's console.log (Safari/Webkit browsers or Firefox w/Firebug installed) * `Bullet.xmpp`: 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. * `Bullet.rails_logger`: add warnings directly to the Rails log * `Bullet.honeybadger`: add notifications to Honeybadger * `Bullet.bugsnag`: add notifications to bugsnag * `Bullet.airbrake`: add notifications to airbrake * `Bullet.appsignal`: add notifications to AppSignal * `Bullet.rollbar`: add notifications to rollbar * `Bullet.sentry`: add notifications to sentry * `Bullet.add_footer`: adds the details in the bottom left corner of the page. Double click the footer or use close button to hide footer. * `Bullet.skip_html_injection`: prevents Bullet from injecting code into the returned HTML. This must be false for receiving alerts, showing the footer or console logging. * `Bullet.skip_http_headers`: don't add headers to API requests, and remove the javascript that relies on them. Note that this prevents bullet from logging warnings to the browser console or updating the footer. * `Bullet.stacktrace_includes`: include paths with any of these substrings in the stack trace, even if they are not in your main app * `Bullet.stacktrace_excludes`: ignore paths with any of these substrings in the stack trace, even if they are not in your main app. Each item can be a string (match substring), a regex, or an array where the first item is a path to match, and the second item is a line number, a Range of line numbers, or a (bare) method name, to exclude only particular lines in a file. * `Bullet.slack`: add notifications to slack * `Bullet.raise`: raise errors, useful for making your specs fail unless they have optimized queries * `Bullet.always_append_html_body`: always append the html body even if no notifications are present. Note: `console` or `add_footer` must also be true. Useful for Single Page Applications where the initial page load might not have any notifications present. Bullet also allows you to disable any of its detectors. ```ruby # Each of these settings defaults to true # Detect N+1 queries Bullet.n_plus_one_query_enable = false # Detect eager-loaded associations which are not used Bullet.unused_eager_loading_enable = false # Detect unnecessary COUNT queries which could be avoided # with a counter_cache Bullet.counter_cache_enable = false ``` Note: When calling `Bullet.enable`, all other detectors are reset to their defaults (`true`) and need reconfiguring. ## Safe list Sometimes Bullet may notify you of query problems you don't care to fix, or which come from outside your code. You can add them to a safe list to ignore them: ```ruby Bullet.add_safelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments Bullet.add_safelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments Bullet.add_safelist :type => :counter_cache, :class_name => "Country", :association => :cities ``` If you want to skip bullet in some specific controller actions, you can do like ```ruby class ApplicationController < ActionController::Base around_action :skip_bullet, if: -> { defined?(Bullet) } def skip_bullet previous_value = Bullet.enable? Bullet.enable = false yield ensure Bullet.enable = previous_value end end ``` ## Log The Bullet log `log/bullet.log` will look something like this: * N+1 Query: ``` 2009-08-25 20:40:17[INFO] USE eager loading detected: Post => [:comments]· Add to your query: .includes([:comments]) 2009-08-25 20:40:17[INFO] Call stack /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each' /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index' ``` The first log entry is a notification that N+1 queries have been encountered. The remaining entry is a stack trace so you can find exactly where the queries were invoked in your code, and fix them. * Unused eager loading: ``` 2009-08-25 20:53:56[INFO] AVOID eager loading detected Post => [:comments]· Remove from your query: .includes([:comments]) 2009-08-25 20:53:56[INFO] Call stack ``` These lines are notifications that unused eager loadings have been encountered. * Need counter cache: ``` 2009-09-11 09:46:50[INFO] Need Counter Cache Post => [:comments] ``` ## XMPP/Jabber and Airbrake Support see [https://github.com/flyerhzm/uniform_notifier](https://github.com/flyerhzm/uniform_notifier) ## Growl Support Growl support is dropped from uniform_notifier 1.16.0, if you still want it, please use uniform_notifier 1.15.0. ## Important If you find Bullet does not work for you, *please disable your browser's cache*. ## Advanced ### Work with ActiveJob Include `Bullet::ActiveJob` in your `ApplicationJob`. ```ruby class ApplicationJob < ActiveJob::Base include Bullet::ActiveJob if Rails.env.development? end ``` ### Work with other background job solution Use the Bullet.profile method. ```ruby class ApplicationJob < ActiveJob::Base around_perform do |_job, block| Bullet.profile do block.call end end end ``` ### Work with sinatra Configure and use `Bullet::Rack`. ```ruby configure :development do Bullet.enable = true Bullet.bullet_logger = true use Bullet::Rack end ``` If your application generates a Content-Security-Policy via a separate middleware, ensure that `Bullet::Rack` is loaded _before_ that middleware. ### Run in tests First you need to enable Bullet in test environment. ```ruby # config/environments/test.rb config.after_initialize do Bullet.enable = true Bullet.bullet_logger = true Bullet.raise = true # raise an error if n+1 query occurs end ``` Then wrap each test in Bullet api. ```ruby # spec/rails_helper.rb if Bullet.enable? config.before(:each) do Bullet.start_request end config.after(:each) do Bullet.perform_out_of_channel_notifications if Bullet.notification? Bullet.end_request end end ``` ## Debug Mode Bullet outputs some details info, to enable debug mode, set `BULLET_DEBUG=true` env. ## Contributors [https://github.com/flyerhzm/bullet/contributors](https://github.com/flyerhzm/bullet/contributors) ## Demo Bullet is designed to function as you browse through your application in development. To see it in action, you can follow these steps to create, detect, and fix example query problems. 1\. Create an example application ``` $ rails new test_bullet $ cd test_bullet $ rails g scaffold post name:string $ rails g scaffold comment name:string post_id:integer $ bundle exec rails db:migrate ``` 2\. Change `app/models/post.rb` and `app/models/comment.rb` ```ruby class Post < ApplicationRecord has_many :comments end class Comment < ApplicationRecord belongs_to :post end ``` 3\. Go to `rails c` and execute ```ruby post1 = Post.create(:name => 'first') post2 = Post.create(:name => 'second') post1.comments.create(:name => 'first') post1.comments.create(:name => 'second') post2.comments.create(:name => 'third') post2.comments.create(:name => 'fourth') ``` 4\. Change the `app/views/posts/index.html.erb` to produce a N+1 query ``` <% @posts.each do |post| %> <%= post.name %> <%= post.comments.map(&:name) %> <%= link_to 'Show', post %> <%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> <% end %> ``` 5\. Add the `bullet` gem to the `Gemfile` ```ruby gem "bullet" ``` And run ``` bundle install ``` 6\. enable the Bullet gem with generate command ``` bundle exec rails g bullet:install ``` 7\. Start the server ``` $ rails s ``` 8\. Visit `http://localhost:3000/posts` in browser, and you will see a popup alert box that says ``` The request has unused preload associations as follows: None The request has N+1 queries as follows: model: Post => associations: [comment] ``` which means there is a N+1 query from the Post object to its Comment association. In the meantime, there's a log appended into `log/bullet.log` file ``` 2010-03-07 14:12:18[INFO] N+1 Query in /posts Post => [:comments] Add to your finder: :include => [:comments] 2010-03-07 14:12:18[INFO] N+1 Query method call stack /home/flyerhzm/Downloads/test_bullet/app/views/posts/index.html.erb:14:in `_render_template__600522146_80203160_0' /home/flyerhzm/Downloads/test_bullet/app/views/posts/index.html.erb:11:in `each' /home/flyerhzm/Downloads/test_bullet/app/views/posts/index.html.erb:11:in `_render_template__600522146_80203160_0' /home/flyerhzm/Downloads/test_bullet/app/controllers/posts_controller.rb:7:in `index' ``` The generated SQL is: ``` Post Load (1.0ms) SELECT * FROM "posts" Comment Load (0.4ms) SELECT * FROM "comments" WHERE ("comments".post_id = 1) Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".post_id = 2) ``` 9\. To fix the N+1 query, change `app/controllers/posts_controller.rb` file ```ruby def index @posts = Post.includes(:comments) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end ``` 10\. Refresh `http://localhost:3000/posts`. Now there's no alert box and nothing new in the log. The generated SQL is: ``` Post Load (0.5ms) SELECT * FROM "posts" Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE ("comments".post_id IN (1,2)) ``` N+1 query fixed. Cool! 11\. Now simulate unused eager loading. Change `app/controllers/posts_controller.rb` and `app/views/posts/index.html.erb` ```ruby def index @posts = Post.includes(:comments) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end ``` ``` <% @posts.each do |post| %> <%= post.name %> <%= link_to 'Show', post %> <%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> <% end %> ``` 12\. Refresh `http://localhost:3000/posts`, and you will see a popup alert box that says ``` The request has unused preload associations as follows: model: Post => associations: [comment] The request has N+1 queries as follows: None ``` Meanwhile, there's a line appended to `log/bullet.log` ``` 2009-08-25 21:13:22[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]· Remove from your finder: :include => [:comments] ``` 13\. Simulate counter_cache. Change `app/controllers/posts_controller.rb` and `app/views/posts/index.html.erb` ```ruby def index @posts = Post.all respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end ``` ``` <% @posts.each do |post| %> <%= post.name %> <%= post.comments.size %> <%= link_to 'Show', post %> <%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %> <% end %> ``` 14\. Refresh `http://localhost:3000/posts`, then you will see a popup alert box that says ``` Need counter cache Post => [:comments] ``` Meanwhile, there's a line appended to `log/bullet.log` ``` 2009-09-11 10:07:10[INFO] Need Counter Cache Post => [:comments] ``` Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com), released under the MIT license bullet-7.1.4/Rakefile000066400000000000000000000022421452565225300144700ustar00rootroot00000000000000$LOAD_PATH.unshift File.expand_path('lib', __dir__) require 'bundler' Bundler.setup require 'rake' require 'rspec' require 'rspec/core/rake_task' require 'bullet/version' task :build do system 'gem build bullet.gemspec' end task install: :build do system "sudo gem install bullet-#{Bullet::VERSION}.gem" end task release: :build do puts "Tagging #{Bullet::VERSION}..." system "git tag -a #{Bullet::VERSION} -m 'Tagging #{Bullet::VERSION}'" puts 'Pushing to Github...' system 'git push --tags' puts 'Pushing to rubygems.org...' system "gem push bullet-#{Bullet::VERSION}.gem" end RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' end RSpec::Core::RakeTask.new('spec:progress') do |spec| spec.rspec_opts = %w[--format progress] spec.pattern = 'spec/**/*_spec.rb' end begin require 'rdoc/task' desc 'Generate documentation for the plugin.' Rake::RDocTask.new do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = "bullet #{Bullet::VERSION}" rdoc.rdoc_files.include('README*') rdoc.rdoc_files.include('lib/**/*.rb') end rescue LoadError puts 'RDocTask is not supported for this platform' end task default: :spec bullet-7.1.4/bullet.gemspec000066400000000000000000000023111452565225300156540ustar00rootroot00000000000000# frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) require 'bullet/version' Gem::Specification.new do |s| s.name = 'bullet' s.version = Bullet::VERSION s.platform = Gem::Platform::RUBY s.authors = ['Richard Huang'] s.email = ['flyerhzm@gmail.com'] s.homepage = 'https://github.com/flyerhzm/bullet' s.summary = 'help to kill N+1 queries and unused eager loading.' s.description = 'help to kill N+1 queries and unused eager loading.' s.metadata = { 'changelog_uri' => 'https://github.com/flyerhzm/bullet/blob/main/CHANGELOG.md', 'source_code_uri' => 'https://github.com/flyerhzm/bullet' } s.license = 'MIT' s.required_ruby_version = '>= 2.3' s.required_rubygems_version = '>= 1.3.6' s.add_runtime_dependency 'activesupport', '>= 3.0.0' s.add_runtime_dependency 'uniform_notifier', '~> 1.11' s.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |file| file.start_with?(*%w[.git .rspec Gemfile Guardfile Hacking Rakefile bullet.gemspec perf rails spec test.sh update.sh]) end end s.require_paths = ['lib'] end bullet-7.1.4/lib/000077500000000000000000000000001452565225300135715ustar00rootroot00000000000000bullet-7.1.4/lib/bullet.rb000066400000000000000000000206341452565225300154120ustar00rootroot00000000000000# frozen_string_literal: true require 'active_support/core_ext/module/delegation' require 'set' require 'uniform_notifier' require 'bullet/ext/object' require 'bullet/ext/string' require 'bullet/dependency' require 'bullet/stack_trace_filter' module Bullet extend Dependency autoload :ActiveRecord, "bullet/#{active_record_version}" if active_record? autoload :Mongoid, "bullet/#{mongoid_version}" if mongoid? autoload :Rack, 'bullet/rack' autoload :ActiveJob, 'bullet/active_job' autoload :Notification, 'bullet/notification' autoload :Detector, 'bullet/detector' autoload :Registry, 'bullet/registry' autoload :NotificationCollector, 'bullet/notification_collector' if defined?(Rails::Railtie) class BulletRailtie < Rails::Railtie initializer 'bullet.configure_rails_initialization' do |app| if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack else app.middleware.use Bullet::Rack end end end end class << self attr_writer :n_plus_one_query_enable, :unused_eager_loading_enable, :counter_cache_enable, :stacktrace_includes, :stacktrace_excludes, :skip_html_injection attr_reader :safelist attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers, :always_append_html_body available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise } .map { |notifier| "#{notifier}=" } available_notifiers_options = { to: UniformNotifier } delegate(*available_notifiers, **available_notifiers_options) def raise=(should_raise) UniformNotifier.raise = (should_raise ? Notification::UnoptimizedQueryError : false) end DETECTORS = [ Bullet::Detector::NPlusOneQuery, Bullet::Detector::UnusedEagerLoading, Bullet::Detector::CounterCache ].freeze def enable=(enable) @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable if enable? reset_safelist unless orm_patches_applied self.orm_patches_applied = true Bullet::Mongoid.enable if mongoid? Bullet::ActiveRecord.enable if active_record? end end end alias enabled= enable= def enable? !!@enable end alias enabled? enable? # Rails.root might be nil if `railties` is a dependency on a project that does not use Rails def app_root @app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s end def n_plus_one_query_enable? enable? && !!@n_plus_one_query_enable end def unused_eager_loading_enable? enable? && !!@unused_eager_loading_enable end def counter_cache_enable? enable? && !!@counter_cache_enable end def stacktrace_includes @stacktrace_includes ||= [] end def stacktrace_excludes @stacktrace_excludes ||= [] end def add_safelist(options) reset_safelist @safelist[options[:type]][options[:class_name]] ||= [] @safelist[options[:type]][options[:class_name]] << options[:association].to_sym end def delete_safelist(options) reset_safelist @safelist[options[:type]][options[:class_name]] ||= [] @safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym) @safelist[options[:type]].delete_if { |_key, val| val.empty? } end def get_safelist_associations(type, class_name) Array.wrap(@safelist[type][class_name]) end def reset_safelist @safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} } end def clear_safelist @safelist = nil end def bullet_logger=(active) if active require 'fileutils' FileUtils.mkdir_p(app_root + '/log') bullet_log_file = File.open("#{app_root}/log/bullet.log", 'a+') bullet_log_file.sync = true UniformNotifier.customized_logger = bullet_log_file end end def debug(title, message) puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true' end def start_request Thread.current[:bullet_start] = true Thread.current[:bullet_notification_collector] = Bullet::NotificationCollector.new Thread.current[:bullet_object_associations] = Bullet::Registry::Base.new Thread.current[:bullet_call_object_associations] = Bullet::Registry::Base.new Thread.current[:bullet_possible_objects] = Bullet::Registry::Object.new Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new end def end_request Thread.current[:bullet_start] = nil Thread.current[:bullet_notification_collector] = nil Thread.current[:bullet_object_associations] = nil Thread.current[:bullet_call_object_associations] = nil Thread.current[:bullet_possible_objects] = nil Thread.current[:bullet_impossible_objects] = nil Thread.current[:bullet_inversed_objects] = nil Thread.current[:bullet_eager_loadings] = nil Thread.current[:bullet_counter_possible_objects] = nil Thread.current[:bullet_counter_impossible_objects] = nil end def start? enable? && Thread.current[:bullet_start] end def notification_collector Thread.current[:bullet_notification_collector] end def notification? return unless start? Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations notification_collector.notifications_present? end def gather_inline_notifications responses = [] for_each_active_notifier_with_notification { |notification| responses << notification.notify_inline } responses.join("\n") end def perform_out_of_channel_notifications(env = {}) request_uri = build_request_uri(env) for_each_active_notifier_with_notification do |notification| notification.url = request_uri notification.notify_out_of_channel end end def footer_info info = [] notification_collector.collection.each { |notification| info << notification.short_notice } info end def text_notifications info = [] notification_collector.collection.each do |notification| info << notification.notification_data.values.compact.join("\n") end info end def warnings notification_collector.collection.each_with_object({}) do |notification, warnings| warning_type = notification.class.to_s.split(':').last.tableize warnings[warning_type] ||= [] warnings[warning_type] << notification end end def profile return_value = nil if Bullet.enable? begin Bullet.start_request return_value = yield Bullet.perform_out_of_channel_notifications if Bullet.notification? ensure Bullet.end_request end else return_value = yield end return_value end def console_enabled? UniformNotifier.active_notifiers.include?(UniformNotifier::JavascriptConsole) end def inject_into_page? return false if defined?(@skip_html_injection) && @skip_html_injection console_enabled? || add_footer end private def for_each_active_notifier_with_notification UniformNotifier.active_notifiers.each do |notifier| notification_collector.collection.each do |notification| notification.notifier = notifier yield notification end end end def build_request_uri(env) return "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']}" if env['REQUEST_URI'] if env['QUERY_STRING'].present? "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}?#{env['QUERY_STRING']}" else "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}" end end end end bullet-7.1.4/lib/bullet/000077500000000000000000000000001452565225300150605ustar00rootroot00000000000000bullet-7.1.4/lib/bullet/active_job.rb000066400000000000000000000003551452565225300175150ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module ActiveJob def self.included(base) base.class_eval do around_perform do |_job, block| Bullet.profile { block.call } end end end end end bullet-7.1.4/lib/bullet/active_record4.rb000066400000000000000000000167641452565225300203200ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.class_eval do class << self alias_method :origin_find_by_sql, :find_by_sql def find_by_sql(sql, binds = []) result = origin_find_by_sql(sql, binds) if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end end ::ActiveRecord::Relation.class_eval do alias_method :origin_to_a, :to_a # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def to_a records = origin_to_a if Bullet.start? if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end records end end ::ActiveRecord::Persistence.class_eval do def _create_record_with_bullet(*args) _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) } end alias_method_chain :_create_record, :bullet end ::ActiveRecord::Associations::Preloader.class_eval do # include query for one to many associations. # keep this eager loadings. alias_method :origin_initialize, :initialize def initialize(records, associations, preload_scope = nil) origin_initialize(records, associations, preload_scope) if Bullet.start? records = [records].flatten.compact.uniq return if records.empty? records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations) end end end ::ActiveRecord::FinderMethods.class_eval do # add includes in scope alias_method :origin_find_with_associations, :find_with_associations def find_with_associations records = origin_find_with_associations if Bullet.start? associations = (eager_load_values + includes_values).uniq records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations) end records end end ::ActiveRecord::Associations::JoinDependency.class_eval do alias_method :origin_instantiate, :instantiate alias_method :origin_construct_association, :construct_association def instantiate(rows) @bullet_eager_loadings = {} records = origin_instantiate(rows) if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a) end end records end # call join associations def construct_association(record, join, row) result = origin_construct_association(record, join, row) if Bullet.start? associations = [join.reflection.name] if join.reflection.nested? associations << join.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ::ActiveRecord::Associations::CollectionAssociation.class_eval do # call one to many associations alias_method :origin_load_target, :load_target def load_target Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? origin_load_target end alias_method :origin_include?, :include? def include?(object) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? origin_include?(object) end end ::ActiveRecord::Associations::HasManyAssociation.class_eval do alias_method :origin_empty?, :empty? def empty? if Bullet.start? && !loaded? && !has_cached_counter?(@reflection) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) end origin_empty? end end ::ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do alias_method :origin_empty?, :empty? def empty? Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? && !loaded? origin_empty? end end ::ActiveRecord::Associations::SingularAssociation.class_eval do # call has_one and belongs_to associations alias_method :origin_reader, :reader def reader(force_reload = false) result = origin_reader(force_reload) if Bullet.start? unless @inversed Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) Bullet::Detector::NPlusOneQuery.add_possible_objects(result) end end result end end ::ActiveRecord::Associations::HasManyAssociation.class_eval do alias_method :origin_has_cached_counter?, :has_cached_counter? def has_cached_counter?(reflection = reflection()) result = origin_has_cached_counter?(reflection) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) if Bullet.start? && !result result end end ::ActiveRecord::Associations::CollectionProxy.class_eval do def count(column_name = nil, options = {}) if Bullet.start? Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name) Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name) end super(column_name, options) end end end end end bullet-7.1.4/lib/bullet/active_record41.rb000066400000000000000000000163441452565225300203730ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.class_eval do class << self alias_method :origin_find_by_sql, :find_by_sql def find_by_sql(sql, binds = []) result = origin_find_by_sql(sql, binds) if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end end ::ActiveRecord::Relation.class_eval do alias_method :origin_to_a, :to_a # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def to_a records = origin_to_a if Bullet.start? if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end end ::ActiveRecord::Persistence.class_eval do def _create_record_with_bullet(*args) _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) } end alias_method_chain :_create_record, :bullet end ::ActiveRecord::Associations::Preloader.class_eval do alias_method :origin_preloaders_on, :preloaders_on def preloaders_on(association, records, scope) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association) end end origin_preloaders_on(association, records, scope) end end ::ActiveRecord::FinderMethods.class_eval do # add includes in scope alias_method :origin_find_with_associations, :find_with_associations def find_with_associations return origin_find_with_associations { |r| yield r } if block_given? records = origin_find_with_associations if Bullet.start? associations = (eager_load_values + includes_values).uniq records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations) end records end end ::ActiveRecord::Associations::JoinDependency.class_eval do alias_method :origin_instantiate, :instantiate alias_method :origin_construct_model, :construct_model def instantiate(result_set, aliases) @bullet_eager_loadings = {} records = origin_instantiate(result_set, aliases) if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a) end end records end # call join associations def construct_model(record, node, row, model_cache, id, aliases) result = origin_construct_model(record, node, row, model_cache, id, aliases) if Bullet.start? associations = [node.reflection.name] if node.reflection.nested? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ::ActiveRecord::Associations::CollectionAssociation.class_eval do # call one to many associations alias_method :origin_load_target, :load_target def load_target Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? && !@inversed origin_load_target end alias_method :origin_empty?, :empty? def empty? if Bullet.start? && !has_cached_counter?(@reflection) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) end origin_empty? end alias_method :origin_include?, :include? def include?(object) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? origin_include?(object) end end ::ActiveRecord::Associations::SingularAssociation.class_eval do # call has_one and belongs_to associations alias_method :origin_reader, :reader def reader(force_reload = false) result = origin_reader(force_reload) if Bullet.start? if @owner.class.name !~ /^HABTM_/ && !@inversed Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) Bullet::Detector::NPlusOneQuery.add_possible_objects(result) end end result end end ::ActiveRecord::Associations::HasManyAssociation.class_eval do alias_method :origin_count_records, :count_records def count_records result = has_cached_counter? Bullet::Detector::CounterCache.add_counter_cache(@owner, @reflection.name) if Bullet.start? && !result origin_count_records end end ::ActiveRecord::Associations::CollectionProxy.class_eval do def count(column_name = nil, options = {}) if Bullet.start? Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name) Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name) end super(column_name, options) end end end end end bullet-7.1.4/lib/bullet/active_record42.rb000066400000000000000000000240041452565225300203640ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.class_eval do class << self alias_method :origin_find, :find def find(*args) result = origin_find(*args) if Bullet.start? if result.is_a? Array Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end alias_method :origin_find_by_sql, :find_by_sql def find_by_sql(sql, binds = []) result = origin_find_by_sql(sql, binds) if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end end ::ActiveRecord::Persistence.class_eval do def _create_record_with_bullet(*args) _create_record_without_bullet(*args).tap { Bullet::Detector::NPlusOneQuery.add_impossible_object(self) } end alias_method_chain :_create_record, :bullet end ::ActiveRecord::Relation.class_eval do alias_method :origin_to_a, :to_a # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def to_a records = origin_to_a if Bullet.start? if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end end ::ActiveRecord::Associations::Preloader.class_eval do alias_method :origin_preloaders_on, :preloaders_on def preloaders_on(association, records, scope) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association) end end origin_preloaders_on(association, records, scope) end end ::ActiveRecord::FinderMethods.class_eval do # add includes in scope alias_method :origin_find_with_associations, :find_with_associations def find_with_associations return origin_find_with_associations { |r| yield r } if block_given? records = origin_find_with_associations if Bullet.start? associations = (eager_load_values + includes_values).uniq records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations) end records end end ::ActiveRecord::Associations::JoinDependency.class_eval do alias_method :origin_instantiate, :instantiate alias_method :origin_construct, :construct alias_method :origin_construct_model, :construct_model def instantiate(result_set, aliases) @bullet_eager_loadings = {} records = origin_instantiate(result_set, aliases) if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings(objects, eager_loadings_hash[objects.first].to_a) end end records end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.nested? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end origin_construct(ar_parent, parent, row, rs, seen, model_cache, aliases) end # call join associations def construct_model(record, node, row, model_cache, id, aliases) result = origin_construct_model(record, node, row, model_cache, id, aliases) if Bullet.start? associations = [node.reflection.name] if node.reflection.nested? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ::ActiveRecord::Associations::CollectionAssociation.class_eval do # call one to many associations alias_method :origin_load_target, :load_target def load_target records = origin_load_target if Bullet.start? Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless @inversed if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end alias_method :origin_empty?, :empty? def empty? if Bullet.start? && !has_cached_counter?(@reflection) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) end origin_empty? end alias_method :origin_include?, :include? def include?(object) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet.start? origin_include?(object) end end ::ActiveRecord::Associations::SingularAssociation.class_eval do # call has_one and belongs_to associations alias_method :origin_reader, :reader def reader(force_reload = false) result = origin_reader(force_reload) if Bullet.start? if @owner.class.name !~ /^HABTM_/ && !@inversed Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(@owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ::ActiveRecord::Associations::HasManyAssociation.class_eval do alias_method :origin_many_empty?, :empty? def empty? result = origin_many_empty? if Bullet.start? && !has_cached_counter?(@reflection) Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) end result end alias_method :origin_count_records, :count_records def count_records result = has_cached_counter? Bullet::Detector::CounterCache.add_counter_cache(@owner, @reflection.name) if Bullet.start? && !result origin_count_records end end ::ActiveRecord::Associations::CollectionProxy.class_eval do def count(column_name = nil, options = {}) if Bullet.start? Bullet::Detector::CounterCache.add_counter_cache(proxy_association.owner, proxy_association.reflection.name) Bullet::Detector::NPlusOneQuery.call_association(proxy_association.owner, proxy_association.reflection.name) end super(column_name, options) end end end end end bullet-7.1.4/lib/bullet/active_record5.rb000066400000000000000000000251561452565225300203140ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module SaveWithBulletSupport def _create_record(*) super do Bullet::Detector::NPlusOneQuery.add_impossible_object(self) yield(self) if block_given? end end end module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.extend( Module.new do def find_by_sql(sql, binds = [], preparable: nil, &block) result = super if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end ) ::ActiveRecord::Base.prepend(SaveWithBulletSupport) ::ActiveRecord::Relation.prepend( Module.new do # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def records result = super if Bullet.start? if result.first.class.name !~ /^HABTM_/ if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end end end result end end ) ::ActiveRecord::Associations::Preloader.prepend( Module.new do def preloaders_for_one(association, records, scope) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association) end end super end end ) ::ActiveRecord::FinderMethods.prepend( Module.new do # add includes in scope def find_with_associations return super { |r| yield r } if block_given? records = super if Bullet.start? associations = (eager_load_values + includes_values).uniq records.each { |record| Bullet::Detector::Association.add_object_associations(record, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, associations) end records end end ) ::ActiveRecord::Associations::JoinDependency.prepend( Module.new do if ::ActiveRecord::Associations::JoinDependency.instance_method(:instantiate).parameters.last[0] == :block # ActiveRecord >= 5.1.5 def instantiate(result_set, &block) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end else # ActiveRecord <= 5.1.4 def instantiate(result_set, aliases) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end super end # call join associations def construct_model(record, node, row, model_cache, id, aliases) result = super if Bullet.start? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ) ::ActiveRecord::Associations::CollectionAssociation.prepend( Module.new do def load_target records = super if Bullet.start? if is_a? ::ActiveRecord::Associations::ThroughAssociation refl = reflection.through_reflection association = owner.association(refl.name) if association.loaded? Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if refl.through_reflection? refl = refl.through_reflection while refl.through_reflection? Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name) end end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end def empty? if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end super end def include?(object) Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start? super end end ) ::ActiveRecord::Associations::SingularAssociation.prepend( Module.new do # call has_one and belongs_to associations def target result = super() if Bullet.start? if owner.class.name !~ /^HABTM_/ && !@inversed Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ) ::ActiveRecord::Associations::HasManyAssociation.prepend( Module.new do def empty? result = super if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end result end def count_records result = reflection.has_cached_counter? if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionProxy.prepend( Module.new do def count if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache( proxy_association.owner, proxy_association.reflection.name ) Bullet::Detector::NPlusOneQuery.call_association( proxy_association.owner, proxy_association.reflection.name ) end super end end ) end end end bullet-7.1.4/lib/bullet/active_record52.rb000066400000000000000000000243211452565225300203670ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module SaveWithBulletSupport def _create_record(*) super do Bullet::Detector::NPlusOneQuery.add_impossible_object(self) yield(self) if block_given? end end end module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.extend( Module.new do def find_by_sql(sql, binds = [], preparable: nil, &block) result = super if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end ) ::ActiveRecord::Base.prepend(SaveWithBulletSupport) ::ActiveRecord::Relation.prepend( Module.new do # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def records result = super if Bullet.start? if result.first.class.name !~ /^HABTM_/ if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end end end result end end ) ::ActiveRecord::Associations::Preloader.prepend( Module.new do def preloaders_for_one(association, records, scope) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association) end end super end end ) ::ActiveRecord::Associations::JoinDependency.prepend( Module.new do def instantiate(result_set, &block) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end super end # call join associations def construct_model(record, node, row, model_cache, id, aliases) result = super if Bullet.start? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ) ::ActiveRecord::Associations::Association.prepend( Module.new do def inversed_from(record) if Bullet.start? Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionAssociation.prepend( Module.new do def load_target records = super if Bullet.start? if is_a? ::ActiveRecord::Associations::ThroughAssociation association = owner.association(reflection.through_reflection.name) if association.loaded? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end def empty? if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end super end def include?(object) Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start? super end end ) ::ActiveRecord::Associations::SingularAssociation.prepend( Module.new do # call has_one and belongs_to associations def target result = super() if Bullet.start? if owner.class.name !~ /^HABTM_/ && !@inversed if is_a? ::ActiveRecord::Associations::ThroughAssociation Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) association = owner.association reflection.through_reflection.name Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ) ::ActiveRecord::Associations::HasManyAssociation.prepend( Module.new do def empty? result = super if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end result end def count_records result = reflection.has_cached_counter? if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionProxy.prepend( Module.new do def count(column_name = nil) if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache( proxy_association.owner, proxy_association.reflection.name ) Bullet::Detector::NPlusOneQuery.call_association( proxy_association.owner, proxy_association.reflection.name ) end super(column_name) end end ) end end end bullet-7.1.4/lib/bullet/active_record60.rb000066400000000000000000000263161452565225300203740ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module SaveWithBulletSupport def _create_record(*) super do Bullet::Detector::NPlusOneQuery.add_impossible_object(self) yield(self) if block_given? end end end module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.extend( Module.new do def find_by_sql(sql, binds = [], preparable: nil, &block) result = super if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end ) ::ActiveRecord::Base.prepend(SaveWithBulletSupport) ::ActiveRecord::Relation.prepend( Module.new do # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def records result = super if Bullet.start? if result.first.class.name !~ /^HABTM_/ if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end end end result end end ) ::ActiveRecord::Associations::Preloader.prepend( Module.new do def preloaders_for_one(association, records, scope, polymorphic_parent) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association) end end super end def preloaders_for_reflection(reflection, records, scope) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, reflection.name) end end super end end ) ::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend( Module.new do def preloaded_records if Bullet.start? && !defined?(@preloaded_records) source_preloaders.each do |source_preloader| reflection_name = source_preloader.send(:reflection).name source_preloader.send(:owners).each do |owner| Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name) end end end super end end ) ::ActiveRecord::Associations::JoinDependency.prepend( Module.new do def instantiate(result_set, &block) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end def construct(ar_parent, parent, row, seen, model_cache) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end super end # call join associations def construct_model(record, node, row, model_cache, id) result = super if Bullet.start? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ) ::ActiveRecord::Associations::Association.prepend( Module.new do def inversed_from(record) if Bullet.start? Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionAssociation.prepend( Module.new do def load_target records = super if Bullet.start? if is_a? ::ActiveRecord::Associations::ThroughAssociation association = owner.association(reflection.through_reflection.name) if association.loaded? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end def empty? if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end super end def include?(object) Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start? super end end ) ::ActiveRecord::Associations::SingularAssociation.prepend( Module.new do # call has_one and belongs_to associations def target result = super() if Bullet.start? if owner.class.name !~ /^HABTM_/ && !@inversed if is_a? ::ActiveRecord::Associations::ThroughAssociation Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) association = owner.association(reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ) ::ActiveRecord::Associations::HasManyAssociation.prepend( Module.new do def empty? result = super if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end result end def count_records result = reflection.has_cached_counter? if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionProxy.prepend( Module.new do def count if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache( proxy_association.owner, proxy_association.reflection.name ) Bullet::Detector::NPlusOneQuery.call_association( proxy_association.owner, proxy_association.reflection.name ) end super end end ) end end end bullet-7.1.4/lib/bullet/active_record61.rb000066400000000000000000000264601452565225300203750ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module SaveWithBulletSupport def _create_record(*) super do Bullet::Detector::NPlusOneQuery.add_impossible_object(self) yield(self) if block_given? end end end module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.extend( Module.new do def find_by_sql(sql, binds = [], preparable: nil, &block) result = super if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end ) ::ActiveRecord::Base.prepend(SaveWithBulletSupport) ::ActiveRecord::Relation.prepend( Module.new do # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def records result = super if Bullet.start? if result.first.class.name !~ /^HABTM_/ if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end end end result end end ) ::ActiveRecord::Associations::Preloader.prepend( Module.new do def preloaders_for_one(association, records, scope, polymorphic_parent) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, association) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, association) end end super end def preloaders_for_reflection(reflection, records, scope) if Bullet.start? records.compact! if records.first.class.name !~ /^HABTM_/ records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(records, reflection.name) end end super end end ) ::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend( Module.new do def preloaded_records if Bullet.start? && !defined?(@preloaded_records) source_preloaders.each do |source_preloader| reflection_name = source_preloader.send(:reflection).name source_preloader.send(:owners).each do |owner| Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name) end end end super end end ) ::ActiveRecord::Associations::JoinDependency.prepend( Module.new do def instantiate(result_set, strict_loading_value, &block) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end super end # call join associations def construct_model(record, node, row, model_cache, id, strict_loading_value) result = super if Bullet.start? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ) ::ActiveRecord::Associations::Association.prepend( Module.new do def inversed_from(record) if Bullet.start? Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionAssociation.prepend( Module.new do def load_target records = super if Bullet.start? if is_a? ::ActiveRecord::Associations::ThroughAssociation association = owner.association(reflection.through_reflection.name) if association.loaded? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end def empty? if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end super end def include?(object) Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start? super end end ) ::ActiveRecord::Associations::SingularAssociation.prepend( Module.new do # call has_one and belongs_to associations def target result = super() if Bullet.start? if owner.class.name !~ /^HABTM_/ && !@inversed if is_a? ::ActiveRecord::Associations::ThroughAssociation Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) association = owner.association(reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ) ::ActiveRecord::Associations::HasManyAssociation.prepend( Module.new do def empty? result = super if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end result end def count_records result = reflection.has_cached_counter? if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionProxy.prepend( Module.new do def count(column_name = nil) if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache( proxy_association.owner, proxy_association.reflection.name ) Bullet::Detector::NPlusOneQuery.call_association( proxy_association.owner, proxy_association.reflection.name ) end super(column_name) end end ) end end end bullet-7.1.4/lib/bullet/active_record70.rb000066400000000000000000000272711452565225300203760ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module SaveWithBulletSupport def _create_record(*) super do Bullet::Detector::NPlusOneQuery.add_impossible_object(self) yield(self) if block_given? end end end module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.extend( Module.new do def find_by_sql(sql, binds = [], preparable: nil, &block) result = super if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end ) ::ActiveRecord::Base.prepend(SaveWithBulletSupport) ::ActiveRecord::Relation.prepend( Module.new do # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def records result = super if Bullet.start? if result.first.class.name !~ /^HABTM_/ if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end end end result end end ) ::ActiveRecord::Associations::Preloader::Batch.prepend( Module.new do def call if Bullet.start? @preloaders.each do |preloader| preloader.records.each { |record| Bullet::Detector::Association.add_object_associations(record, preloader.associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations) end end super end end ) ::ActiveRecord::Associations::Preloader::Branch.prepend( Module.new do def preloaders_for_reflection(reflection, reflection_records) if Bullet.start? reflection_records.compact! if reflection_records.first.class.name !~ /^HABTM_/ reflection_records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name) end end super end end ) ::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend( Module.new do def source_preloaders if Bullet.start? && !defined?(@source_preloaders) preloaders = super preloaders.each do |preloader| reflection_name = preloader.send(:reflection).name preloader.send(:owners).each do |owner| Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name) end end else super end end end ) ::ActiveRecord::Associations::JoinDependency.prepend( Module.new do def instantiate(result_set, strict_loading_value, &block) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end super end # call join associations def construct_model(record, node, row, model_cache, id, strict_loading_value) result = super if Bullet.start? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ) ::ActiveRecord::Associations::Association.prepend( Module.new do def inversed_from(record) if Bullet.start? Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end def inversed_from_queries(record) if Bullet.start? && inversable?(record) Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionAssociation.prepend( Module.new do def load_target records = super if Bullet.start? if is_a? ::ActiveRecord::Associations::ThroughAssociation association = owner.association(reflection.through_reflection.name) if association.loaded? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end def empty? if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end super end def include?(object) Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start? super end end ) ::ActiveRecord::Associations::SingularAssociation.prepend( Module.new do # call has_one and belongs_to associations def reader result = super if Bullet.start? if owner.class.name !~ /^HABTM_/ if is_a? ::ActiveRecord::Associations::ThroughAssociation Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) association = owner.association(reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ) ::ActiveRecord::Associations::HasManyAssociation.prepend( Module.new do def empty? result = super if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end result end def count_records result = reflection.has_cached_counter? if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionProxy.prepend( Module.new do def count(column_name = nil) if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache( proxy_association.owner, proxy_association.reflection.name ) Bullet::Detector::NPlusOneQuery.call_association( proxy_association.owner, proxy_association.reflection.name ) end super(column_name) end end ) end end end bullet-7.1.4/lib/bullet/active_record71.rb000066400000000000000000000272711452565225300203770ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module SaveWithBulletSupport def _create_record(*) super do Bullet::Detector::NPlusOneQuery.add_impossible_object(self) yield(self) if block_given? end end end module ActiveRecord def self.enable require 'active_record' ::ActiveRecord::Base.extend( Module.new do def find_by_sql(sql, binds = [], preparable: nil, &block) result = super if Bullet.start? if result.is_a? Array if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end elsif result.is_a? ::ActiveRecord::Base Bullet::Detector::NPlusOneQuery.add_impossible_object(result) Bullet::Detector::CounterCache.add_impossible_object(result) end end result end end ) ::ActiveRecord::Base.prepend(SaveWithBulletSupport) ::ActiveRecord::Relation.prepend( Module.new do # if select a collection of objects, then these objects have possible to cause N+1 query. # if select only one object, then the only one object has impossible to cause N+1 query. def records result = super if Bullet.start? if result.first.class.name !~ /^HABTM_/ if result.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(result) Bullet::Detector::CounterCache.add_possible_objects(result) elsif result.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(result.first) Bullet::Detector::CounterCache.add_impossible_object(result.first) end end end result end end ) ::ActiveRecord::Associations::Preloader::Batch.prepend( Module.new do def call if Bullet.start? @preloaders.each do |preloader| preloader.records.each { |record| Bullet::Detector::Association.add_object_associations(record, preloader.associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(preloader.records, preloader.associations) end end super end end ) ::ActiveRecord::Associations::Preloader::Branch.prepend( Module.new do def preloaders_for_reflection(reflection, reflection_records) if Bullet.start? reflection_records.compact! if reflection_records.first.class.name !~ /^HABTM_/ reflection_records.each { |record| Bullet::Detector::Association.add_object_associations(record, reflection.name) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(reflection_records, reflection.name) end end super end end ) ::ActiveRecord::Associations::Preloader::ThroughAssociation.prepend( Module.new do def source_preloaders if Bullet.start? && !defined?(@source_preloaders) preloaders = super preloaders.each do |preloader| reflection_name = preloader.send(:reflection).name preloader.send(:owners).each do |owner| Bullet::Detector::NPlusOneQuery.call_association(owner, reflection_name) end end else super end end end ) ::ActiveRecord::Associations::JoinDependency.prepend( Module.new do def instantiate(result_set, strict_loading_value, &block) @bullet_eager_loadings = {} records = super if Bullet.start? @bullet_eager_loadings.each do |_klazz, eager_loadings_hash| objects = eager_loadings_hash.keys Bullet::Detector::UnusedEagerLoading.add_eager_loadings( objects, eager_loadings_hash[objects.first].to_a ) end end records end def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value) if Bullet.start? unless ar_parent.nil? parent.children.each do |node| key = aliases.column_alias(node, node.primary_key) id = row[key] next unless id.nil? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(ar_parent, association) Bullet::Detector::NPlusOneQuery.call_association(ar_parent, association) @bullet_eager_loadings[ar_parent.class] ||= {} @bullet_eager_loadings[ar_parent.class][ar_parent] ||= Set.new @bullet_eager_loadings[ar_parent.class][ar_parent] << association end end end end super end # call join associations def construct_model(record, node, row, model_cache, id, strict_loading_value) result = super if Bullet.start? associations = [node.reflection.name] if node.reflection.through_reflection? associations << node.reflection.through_reflection.name end associations.each do |association| Bullet::Detector::Association.add_object_associations(record, association) Bullet::Detector::NPlusOneQuery.call_association(record, association) @bullet_eager_loadings[record.class] ||= {} @bullet_eager_loadings[record.class][record] ||= Set.new @bullet_eager_loadings[record.class][record] << association end end result end end ) ::ActiveRecord::Associations::Association.prepend( Module.new do def inversed_from(record) if Bullet.start? Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end def inversed_from_queries(record) if Bullet.start? && inversable?(record) Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionAssociation.prepend( Module.new do def load_target records = super if Bullet.start? if is_a? ::ActiveRecord::Associations::ThroughAssociation association = owner.association(reflection.through_reflection.name) if association.loaded? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if records.first.class.name !~ /^HABTM_/ if records.size > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) Bullet::Detector::CounterCache.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) Bullet::Detector::CounterCache.add_impossible_object(records.first) end end end records end def empty? if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end super end def include?(object) Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet.start? super end end ) ::ActiveRecord::Associations::SingularAssociation.prepend( Module.new do # call has_one and belongs_to associations def reader result = super if Bullet.start? if owner.class.name !~ /^HABTM_/ if is_a? ::ActiveRecord::Associations::ThroughAssociation Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name) association = owner.association(reflection.through_reflection.name) Array.wrap(association.target).each do |through_record| Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name) end if reflection.through_reflection != through_reflection Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name) end end Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) if Bullet::Detector::NPlusOneQuery.impossible?(owner) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result else Bullet::Detector::NPlusOneQuery.add_possible_objects(result) if result end end end result end end ) ::ActiveRecord::Associations::HasManyAssociation.prepend( Module.new do def empty? result = super if Bullet.start? && !reflection.has_cached_counter? Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) end result end def count_records result = reflection.has_cached_counter? if Bullet.start? && !result && !is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache(owner, reflection.name) end super end end ) ::ActiveRecord::Associations::CollectionProxy.prepend( Module.new do def count(column_name = nil) if Bullet.start? && !proxy_association.is_a?(::ActiveRecord::Associations::ThroughAssociation) Bullet::Detector::CounterCache.add_counter_cache( proxy_association.owner, proxy_association.reflection.name ) Bullet::Detector::NPlusOneQuery.call_association( proxy_association.owner, proxy_association.reflection.name ) end super(column_name) end end ) end end end bullet-7.1.4/lib/bullet/bullet_xhr.js000066400000000000000000000046031452565225300175710ustar00rootroot00000000000000(function () { var oldOpen = window.XMLHttpRequest.prototype.open; var oldSend = window.XMLHttpRequest.prototype.send; /** * Return early if we've already extended prototype. This prevents * "maximum call stack exceeded" errors when used with Turbolinks. * See https://github.com/flyerhzm/bullet/issues/454 */ if (isBulletInitiated()) return; function isBulletInitiated() { return oldOpen.name == "bulletXHROpen" && oldSend.name == "bulletXHRSend"; } function bulletXHROpen(_, url) { this._storedUrl = url; return Reflect.apply(oldOpen, this, arguments); } function bulletXHRSend() { if (this.onload) { this._storedOnload = this.onload; } this.onload = null; this.addEventListener("load", bulletXHROnload); return Reflect.apply(oldSend, this, arguments); } function bulletXHROnload() { if ( this._storedUrl.startsWith(window.location.protocol + "//" + window.location.host) || !this._storedUrl.startsWith("http") // For relative paths ) { var bulletFooterText = this.getResponseHeader("X-bullet-footer-text"); if (bulletFooterText) { setTimeout(function () { var oldHtml = document.querySelector("#bullet-footer").innerHTML.split("
"); var header = oldHtml[0]; oldHtml = oldHtml.slice(1, oldHtml.length); var newHtml = oldHtml.concat(JSON.parse(bulletFooterText)); newHtml = newHtml.slice(newHtml.length - 10, newHtml.length); // rotate through 10 most recent document.querySelector("#bullet-footer").innerHTML = `${header}
${newHtml.join("
")}`; }, 0); } var bulletConsoleText = this.getResponseHeader("X-bullet-console-text"); if (bulletConsoleText && typeof console !== "undefined" && console.log) { setTimeout(function () { JSON.parse(bulletConsoleText).forEach((message) => { if (console.groupCollapsed && console.groupEnd) { console.groupCollapsed("Uniform Notifier"); console.log(message); console.groupEnd(); } else { console.log(message); } }); }, 0); } } if (this._storedOnload) { return Reflect.apply(this._storedOnload, this, arguments); } } window.XMLHttpRequest.prototype.open = bulletXHROpen; window.XMLHttpRequest.prototype.send = bulletXHRSend; })(); bullet-7.1.4/lib/bullet/dependency.rb000066400000000000000000000062061452565225300175270ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Dependency def mongoid? @mongoid ||= defined?(::Mongoid) end def active_record? @active_record ||= defined?(::ActiveRecord) end def active_record_version @active_record_version ||= begin if active_record40? 'active_record4' elsif active_record41? 'active_record41' elsif active_record42? 'active_record42' elsif active_record50? 'active_record5' elsif active_record51? 'active_record5' elsif active_record52? 'active_record52' elsif active_record60? 'active_record60' elsif active_record61? 'active_record61' elsif active_record70? 'active_record70' elsif active_record71? 'active_record71' else raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet" end end end def mongoid_version @mongoid_version ||= begin if mongoid4x? 'mongoid4x' elsif mongoid5x? 'mongoid5x' elsif mongoid6x? 'mongoid6x' elsif mongoid7x? 'mongoid7x' elsif mongoid8x? 'mongoid8x' else raise "Bullet does not support mongoid #{::Mongoid::VERSION} yet" end end end def active_record4? active_record? && ::ActiveRecord::VERSION::MAJOR == 4 end def active_record5? active_record? && ::ActiveRecord::VERSION::MAJOR == 5 end def active_record6? active_record? && ::ActiveRecord::VERSION::MAJOR == 6 end def active_record7? active_record? && ::ActiveRecord::VERSION::MAJOR == 7 end def active_record40? active_record4? && ::ActiveRecord::VERSION::MINOR == 0 end def active_record41? active_record4? && ::ActiveRecord::VERSION::MINOR == 1 end def active_record42? active_record4? && ::ActiveRecord::VERSION::MINOR == 2 end def active_record50? active_record5? && ::ActiveRecord::VERSION::MINOR == 0 end def active_record51? active_record5? && ::ActiveRecord::VERSION::MINOR == 1 end def active_record52? active_record5? && ::ActiveRecord::VERSION::MINOR == 2 end def active_record60? active_record6? && ::ActiveRecord::VERSION::MINOR == 0 end def active_record61? active_record6? && ::ActiveRecord::VERSION::MINOR == 1 end def active_record70? active_record7? && ::ActiveRecord::VERSION::MINOR == 0 end def active_record71? active_record7? && ::ActiveRecord::VERSION::MINOR == 1 end def mongoid4x? mongoid? && ::Mongoid::VERSION =~ /\A4/ end def mongoid5x? mongoid? && ::Mongoid::VERSION =~ /\A5/ end def mongoid6x? mongoid? && ::Mongoid::VERSION =~ /\A6/ end def mongoid7x? mongoid? && ::Mongoid::VERSION =~ /\A7/ end def mongoid8x? mongoid? && ::Mongoid::VERSION =~ /\A8/ end end end bullet-7.1.4/lib/bullet/detector.rb000066400000000000000000000005621452565225300172210ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Detector autoload :Base, 'bullet/detector/base' autoload :Association, 'bullet/detector/association' autoload :NPlusOneQuery, 'bullet/detector/n_plus_one_query' autoload :UnusedEagerLoading, 'bullet/detector/unused_eager_loading' autoload :CounterCache, 'bullet/detector/counter_cache' end end bullet-7.1.4/lib/bullet/detector/000077500000000000000000000000001452565225300166715ustar00rootroot00000000000000bullet-7.1.4/lib/bullet/detector/association.rb000066400000000000000000000065111452565225300215350ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Detector class Association < Base class << self def add_object_associations(object, associations) return unless Bullet.start? return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable? return unless object.bullet_primary_key_value Bullet.debug( 'Detector::Association#add_object_associations', "object: #{object.bullet_key}, associations: #{associations}" ) call_stacks.add(object.bullet_key) object_associations.add(object.bullet_key, associations) end def add_call_object_associations(object, associations) return unless Bullet.start? return if !Bullet.n_plus_one_query_enable? && !Bullet.unused_eager_loading_enable? return unless object.bullet_primary_key_value Bullet.debug( 'Detector::Association#add_call_object_associations', "object: #{object.bullet_key}, associations: #{associations}" ) call_stacks.add(object.bullet_key) call_object_associations.add(object.bullet_key, associations) end # possible_objects keep the class to object relationships # that the objects may cause N+1 query. # e.g. { Post => ["Post:1", "Post:2"] } def possible_objects Thread.current[:bullet_possible_objects] end # impossible_objects keep the class to objects relationships # that the objects may not cause N+1 query. # e.g. { Post => ["Post:1", "Post:2"] } # if find collection returns only one object, then the object is impossible object, # impossible_objects are used to avoid treating 1+1 query to N+1 query. def impossible_objects Thread.current[:bullet_impossible_objects] end private # object_associations keep the object relationships # that the object has many associations. # e.g. { "Post:1" => [:comments] } # the object_associations keep all associations that may be or may no be # unpreload associations or unused preload associations. def object_associations Thread.current[:bullet_object_associations] end # call_object_associations keep the object relationships # that object.associations is called. # e.g. { "Post:1" => [:comments] } # they are used to detect unused preload associations. def call_object_associations Thread.current[:bullet_call_object_associations] end # inversed_objects keeps object relationships # that association is inversed. # e.g. { "Comment:1" => ["post"] } def inversed_objects Thread.current[:bullet_inversed_objects] end # eager_loadings keep the object relationships # that the associations are preloaded by find :include. # e.g. { ["Post:1", "Post:2"] => [:comments, :user] } def eager_loadings Thread.current[:bullet_eager_loadings] end # cal_stacks keeps stacktraces where querie-objects were called from. # e.g. { 'Object:111' => [SomeProject/app/controllers/...] } def call_stacks Thread.current[:bullet_call_stacks] end end end end end bullet-7.1.4/lib/bullet/detector/base.rb000066400000000000000000000001401452565225300201230ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Detector class Base end end end bullet-7.1.4/lib/bullet/detector/counter_cache.rb000066400000000000000000000043321452565225300220220ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Detector class CounterCache < Base class << self def add_counter_cache(object, associations) return unless Bullet.start? return unless Bullet.counter_cache_enable? return unless object.bullet_primary_key_value Bullet.debug( 'Detector::CounterCache#add_counter_cache', "object: #{object.bullet_key}, associations: #{associations}" ) create_notification object.class.to_s, associations if conditions_met?(object, associations) end def add_possible_objects(object_or_objects) return unless Bullet.start? return unless Bullet.counter_cache_enable? objects = Array.wrap(object_or_objects) return if objects.map(&:bullet_primary_key_value).compact.empty? Bullet.debug( 'Detector::CounterCache#add_possible_objects', "objects: #{objects.map(&:bullet_key).join(', ')}" ) objects.each { |object| possible_objects.add object.bullet_key } end def add_impossible_object(object) return unless Bullet.start? return unless Bullet.counter_cache_enable? return unless object.bullet_primary_key_value Bullet.debug('Detector::CounterCache#add_impossible_object', "object: #{object.bullet_key}") impossible_objects.add object.bullet_key end def conditions_met?(object, _associations) possible_objects.include?(object.bullet_key) && !impossible_objects.include?(object.bullet_key) end def possible_objects Thread.current[:bullet_counter_possible_objects] end def impossible_objects Thread.current[:bullet_counter_impossible_objects] end private def create_notification(klazz, associations) notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:counter_cache, klazz) if notify_associations.present? notice = Bullet::Notification::CounterCache.new klazz, notify_associations Bullet.notification_collector.add notice end end end end end end bullet-7.1.4/lib/bullet/detector/n_plus_one_query.rb000066400000000000000000000106101452565225300226020ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Detector class NPlusOneQuery < Association extend Dependency extend StackTraceFilter class << self # executed when object.associations is called. # first, it keeps this method call for object.association. # then, it checks if this associations call is unpreload. # if it is, keeps this unpreload associations and caller. def call_association(object, associations) return unless Bullet.start? return unless Bullet.n_plus_one_query_enable? return unless object.bullet_primary_key_value return if inversed_objects.include?(object.bullet_key, associations) add_call_object_associations(object, associations) Bullet.debug( 'Detector::NPlusOneQuery#call_association', "object: #{object.bullet_key}, associations: #{associations}" ) if !excluded_stacktrace_path? && conditions_met?(object, associations) Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}") create_notification caller_in_project(object.bullet_key), object.class.to_s, associations end end def add_possible_objects(object_or_objects) return unless Bullet.start? return unless Bullet.n_plus_one_query_enable? objects = Array.wrap(object_or_objects) class_names_match_regex = true primary_key_values_are_empty = true keys_joined = "" objects.each do |obj| unless obj.class.name =~ /^HABTM_/ class_names_match_regex = false end unless obj.bullet_primary_key_value.nil? primary_key_values_are_empty = false end keys_joined += "#{(keys_joined.empty? ? '' : ', ')}#{obj.bullet_key}" end unless class_names_match_regex || primary_key_values_are_empty Bullet.debug('Detector::NPlusOneQuery#add_possible_objects', "objects: #{keys_joined}") objects.each { |object| possible_objects.add object.bullet_key } end end def add_impossible_object(object) return unless Bullet.start? return unless Bullet.n_plus_one_query_enable? return unless object.bullet_primary_key_value Bullet.debug('Detector::NPlusOneQuery#add_impossible_object', "object: #{object.bullet_key}") impossible_objects.add object.bullet_key end def add_inversed_object(object, association) return unless Bullet.start? return unless Bullet.n_plus_one_query_enable? return unless object.bullet_primary_key_value Bullet.debug( 'Detector::NPlusOneQuery#add_inversed_object', "object: #{object.bullet_key}, association: #{association}" ) inversed_objects.add object.bullet_key, association end # decide whether the object.associations is unpreloaded or not. def conditions_met?(object, associations) possible?(object) && !impossible?(object) && !association?(object, associations) end def possible?(object) possible_objects.include? object.bullet_key end def impossible?(object) impossible_objects.include? object.bullet_key end # check if object => associations already exists in object_associations. def association?(object, associations) value = object_associations[object.bullet_key] value&.each do |v| # associations == v comparison order is important here because # v variable might be a squeel node where :== method is redefined, # so it does not compare values at all and return unexpected results result = v.is_a?(Hash) ? v.key?(associations) : associations == v return true if result end false end private def create_notification(callers, klazz, associations) notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz) if notify_associations.present? notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations) Bullet.notification_collector.add(notice) end end end end end end bullet-7.1.4/lib/bullet/detector/unused_eager_loading.rb000066400000000000000000000071001452565225300233570ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Detector class UnusedEagerLoading < Association extend Dependency extend StackTraceFilter class << self # check if there are unused preload associations. # get related_objects from eager_loadings associated with object and associations # get call_object_association from associations of call_object_associations whose object is in related_objects # if association not in call_object_association, then the object => association - call_object_association is unused preload associations def check_unused_preload_associations return unless Bullet.start? return unless Bullet.unused_eager_loading_enable? object_associations.each do |bullet_key, associations| object_association_diff = diff_object_associations bullet_key, associations next if object_association_diff.empty? Bullet.debug('detect unused preload', "object: #{bullet_key}, associations: #{object_association_diff}") create_notification(caller_in_project(bullet_key), bullet_key.bullet_class_name, object_association_diff) end end def add_eager_loadings(objects, associations) return unless Bullet.start? return unless Bullet.unused_eager_loading_enable? return if objects.map(&:bullet_primary_key_value).compact.empty? Bullet.debug( 'Detector::UnusedEagerLoading#add_eager_loadings', "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}" ) bullet_keys = objects.map(&:bullet_key) to_add = [] to_merge = [] to_delete = [] eager_loadings.each do |k, _v| key_objects_overlap = k & bullet_keys next if key_objects_overlap.empty? bullet_keys -= k if key_objects_overlap == k to_add << [k, associations] else to_merge << [key_objects_overlap, (eager_loadings[k].dup << associations)] keys_without_objects = k - key_objects_overlap to_merge << [keys_without_objects, eager_loadings[k]] to_delete << k end end to_add.each { |k, val| eager_loadings.add k, val } to_merge.each { |k, val| eager_loadings.merge k, val } to_delete.each { |k| eager_loadings.delete k } eager_loadings.add bullet_keys, associations unless bullet_keys.empty? end private def create_notification(callers, klazz, associations) notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations( :unused_eager_loading, klazz ) if notify_associations.present? notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations) Bullet.notification_collector.add(notice) end end def call_associations(bullet_key, associations) all = Set.new eager_loadings.similarly_associated(bullet_key, associations).each do |related_bullet_key| coa = call_object_associations[related_bullet_key] next if coa.nil? all.merge coa end all.to_a end def diff_object_associations(bullet_key, associations) potential_associations = associations - call_associations(bullet_key, associations) potential_associations.reject { |a| a.is_a?(Hash) } end end end end end bullet-7.1.4/lib/bullet/ext/000077500000000000000000000000001452565225300156605ustar00rootroot00000000000000bullet-7.1.4/lib/bullet/ext/object.rb000066400000000000000000000014151452565225300174540ustar00rootroot00000000000000# frozen_string_literal: true class Object def bullet_key "#{self.class}:#{bullet_primary_key_value}" end def bullet_primary_key_value return if respond_to?(:persisted?) && !persisted? if self.class.respond_to?(:primary_keys) && self.class.primary_keys primary_key = self.class.primary_keys elsif self.class.respond_to?(:primary_key) && self.class.primary_key primary_key = self.class.primary_key else primary_key = :id end bullet_join_potential_composite_primary_key(primary_key) end private def bullet_join_potential_composite_primary_key(primary_keys) return send(primary_keys) unless primary_keys.is_a?(Enumerable) primary_keys.map { |primary_key| send primary_key } .join(',') end end bullet-7.1.4/lib/bullet/ext/string.rb000066400000000000000000000001461452565225300175140ustar00rootroot00000000000000# frozen_string_literal: true class String def bullet_class_name sub(/:[^:]*?$/, '') end end bullet-7.1.4/lib/bullet/mongoid4x.rb000066400000000000000000000034241452565225300173200ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Mongoid def self.enable require 'mongoid' ::Mongoid::Contextual::Mongo.class_eval do alias_method :origin_first, :first alias_method :origin_last, :last alias_method :origin_each, :each alias_method :origin_eager_load, :eager_load def first result = origin_first Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def last result = origin_last Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def each(&block) return to_enum unless block records = [] origin_each { |record| records << record } if records.length > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) end records.each(&block) end def eager_load(docs) associations = criteria.inclusions.map(&:name) docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) origin_eager_load(docs) end end ::Mongoid::Relations::Accessors.class_eval do alias_method :origin_get_relation, :get_relation def get_relation(name, metadata, object, reload = false) result = origin_get_relation(name, metadata, object, reload) Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/ result end end end end end bullet-7.1.4/lib/bullet/mongoid5x.rb000066400000000000000000000034241452565225300173210ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Mongoid def self.enable require 'mongoid' ::Mongoid::Contextual::Mongo.class_eval do alias_method :origin_first, :first alias_method :origin_last, :last alias_method :origin_each, :each alias_method :origin_eager_load, :eager_load def first result = origin_first Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def last result = origin_last Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def each(&block) return to_enum unless block records = [] origin_each { |record| records << record } if records.length > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) end records.each(&block) end def eager_load(docs) associations = criteria.inclusions.map(&:name) docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) origin_eager_load(docs) end end ::Mongoid::Relations::Accessors.class_eval do alias_method :origin_get_relation, :get_relation def get_relation(name, metadata, object, reload = false) result = origin_get_relation(name, metadata, object, reload) Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/ result end end end end end bullet-7.1.4/lib/bullet/mongoid6x.rb000066400000000000000000000034621452565225300173240ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Mongoid def self.enable require 'mongoid' ::Mongoid::Contextual::Mongo.class_eval do alias_method :origin_first, :first alias_method :origin_last, :last alias_method :origin_each, :each alias_method :origin_eager_load, :eager_load def first(opt = {}) result = origin_first(opt) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def last(opt = {}) result = origin_last(opt) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def each(&block) return to_enum unless block records = [] origin_each { |record| records << record } if records.length > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) end records.each(&block) end def eager_load(docs) associations = criteria.inclusions.map(&:name) docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) origin_eager_load(docs) end end ::Mongoid::Relations::Accessors.class_eval do alias_method :origin_get_relation, :get_relation def get_relation(name, metadata, object, reload = false) result = origin_get_relation(name, metadata, object, reload) Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/ result end end end end end bullet-7.1.4/lib/bullet/mongoid7x.rb000066400000000000000000000044361452565225300173270ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Mongoid def self.enable require 'mongoid' require 'rubygems' ::Mongoid::Contextual::Mongo.class_eval do alias_method :origin_first, :first alias_method :origin_last, :last alias_method :origin_each, :each alias_method :origin_eager_load, :eager_load %i[first last].each do |context| default = Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('7.5') ? nil : {} define_method(context) do |opts = default| result = send(:"origin_#{context}", opts) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end end def each(&block) return to_enum unless block_given? first_document = nil document_count = 0 origin_each do |document| document_count += 1 if document_count == 1 first_document = document elsif document_count == 2 Bullet::Detector::NPlusOneQuery.add_possible_objects([first_document, document]) yield(first_document) first_document = nil yield(document) else Bullet::Detector::NPlusOneQuery.add_possible_objects(document) yield(document) end end if document_count == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(first_document) yield(first_document) end self end def eager_load(docs) associations = criteria.inclusions.map(&:name) docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) } Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) origin_eager_load(docs) end end ::Mongoid::Association::Accessors.class_eval do alias_method :origin_get_relation, :get_relation def get_relation(name, association, object, reload = false) result = origin_get_relation(name, association, object, reload) Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded? result end end end end end bullet-7.1.4/lib/bullet/mongoid8x.rb000066400000000000000000000035311452565225300173230ustar00rootroot00000000000000module Bullet module Mongoid def self.enable require 'mongoid' ::Mongoid::Contextual::Mongo.class_eval do alias_method :origin_first, :first alias_method :origin_last, :last alias_method :origin_each, :each alias_method :origin_eager_load, :eager_load def first(opts = {}) result = origin_first(opts) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def last(opts = {}) result = origin_last(opts) Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result result end def each(&block) return to_enum unless block_given? records = [] origin_each { |record| records << record } if records.length > 1 Bullet::Detector::NPlusOneQuery.add_possible_objects(records) elsif records.size == 1 Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first) end records.each(&block) end def eager_load(docs) associations = criteria.inclusions.map(&:name) docs.each do |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) end Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations) origin_eager_load(docs) end end ::Mongoid::Association::Accessors.class_eval do alias_method :origin_get_relation, :get_relation def get_relation(name, association, object, reload = false) result = origin_get_relation(name, association, object, reload) unless association.embedded? Bullet::Detector::NPlusOneQuery.call_association(self, name) end result end end end end end bullet-7.1.4/lib/bullet/notification.rb000066400000000000000000000006061452565225300200750ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Notification autoload :Base, 'bullet/notification/base' autoload :UnusedEagerLoading, 'bullet/notification/unused_eager_loading' autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query' autoload :CounterCache, 'bullet/notification/counter_cache' class UnoptimizedQueryError < StandardError end end end bullet-7.1.4/lib/bullet/notification/000077500000000000000000000000001452565225300175465ustar00rootroot00000000000000bullet-7.1.4/lib/bullet/notification/base.rb000066400000000000000000000035131452565225300210070ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Notification class Base attr_accessor :notifier, :url attr_reader :base_class, :associations, :path def initialize(base_class, association_or_associations, path = nil) @base_class = base_class @associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations] @path = path end def title raise NoMethodError, 'no method title defined' end def body raise NoMethodError, 'no method body defined' end def call_stack_messages '' end def whoami @user ||= ENV['USER'].presence || ( begin `whoami`.chomp rescue StandardError '' end ) @user.present? ? "user: #{@user}" : '' end def body_with_caller "#{body}\n#{call_stack_messages}\n" end def notify_inline notifier.inline_notify(notification_data) end def notify_out_of_channel notifier.out_of_channel_notify(notification_data) end def short_notice [whoami.presence, url, title, body].compact.join(' ') end def notification_data { user: whoami, url: url, title: title, body: body_with_caller } end def eql?(other) self.class == other.class && klazz_associations_str == other.klazz_associations_str end def hash [self.class, klazz_associations_str].hash end protected def klazz_associations_str " #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]" end def associations_str ".includes(#{@associations.map { |a| a.to_s.to_sym } .inspect})" end end end end bullet-7.1.4/lib/bullet/notification/counter_cache.rb000066400000000000000000000003731452565225300227000ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Notification class CounterCache < Base def body klazz_associations_str end def title 'Need Counter Cache with Active Record size' end end end end bullet-7.1.4/lib/bullet/notification/n_plus_one_query.rb000066400000000000000000000012061452565225300234600ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Notification class NPlusOneQuery < Base def initialize(callers, base_class, associations, path = nil) super(base_class, associations, path) @callers = callers end def body "#{klazz_associations_str}\n Add to your query: #{associations_str}" end def title "USE eager loading #{@path ? "in #{@path}" : 'detected'}" end def notification_data super.merge(backtrace: []) end protected def call_stack_messages (['Call stack'] + @callers).join("\n ") end end end end bullet-7.1.4/lib/bullet/notification/unused_eager_loading.rb000066400000000000000000000012221452565225300242330ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Notification class UnusedEagerLoading < Base def initialize(callers, base_class, associations, path = nil) super(base_class, associations, path) @callers = callers end def body "#{klazz_associations_str}\n Remove from your query: #{associations_str}" end def title "AVOID eager loading #{@path ? "in #{@path}" : 'detected'}" end def notification_data super.merge(backtrace: []) end protected def call_stack_messages (['Call stack'] + @callers).join("\n ") end end end end bullet-7.1.4/lib/bullet/notification_collector.rb000066400000000000000000000005241452565225300221420ustar00rootroot00000000000000# frozen_string_literal: true require 'set' module Bullet class NotificationCollector attr_reader :collection def initialize reset end def reset @collection = Set.new end def add(value) @collection << value end def notifications_present? !@collection.empty? end end end bullet-7.1.4/lib/bullet/rack.rb000066400000000000000000000121031452565225300163220ustar00rootroot00000000000000# frozen_string_literal: true module Bullet class Rack include Dependency NONCE_MATCHER = /script-src .*'nonce-(?[A-Za-z0-9+\/]+={0,2})'/ def initialize(app) @app = app end def call(env) return @app.call(env) unless Bullet.enable? Bullet.start_request status, headers, response = @app.call(env) response_body = nil if Bullet.notification? || Bullet.always_append_html_body if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200 if html_request?(headers, response) response_body = response_body(response) with_security_policy_nonce(headers) do |nonce| response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications) if Bullet.add_footer && !Bullet.skip_http_headers response_body = append_to_html_body(response_body, xhr_script(nonce)) end end headers['Content-Length'] = response_body.bytesize.to_s elsif !Bullet.skip_http_headers set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled? end end Bullet.perform_out_of_channel_notifications(env) end [status, headers, response_body ? [response_body] : response] ensure Bullet.end_request end # fix issue if response's body is a Proc def empty?(response) # response may be ["Not Found"], ["Move Permanently"], etc, but # those should not happen if the status is 200 return true if !response.respond_to?(:body) && !response.respond_to?(:first) body = response_body(response) body.nil? || body.empty? end def append_to_html_body(response_body, content) body = response_body.dup content = content.html_safe if content.respond_to?(:html_safe) if body.include?('') position = body.rindex('') body.insert(position, content) else body << content end end def footer_note "
Bullet Warnings
#{Bullet.footer_info.uniq.join('
')}#{footer_console_message}
" end def set_header(headers, header_name, header_array) # Many proxy applications such as Nginx and AWS ELB limit # the size a header to 8KB, so truncate the list of reports to # be under that limit header_array.pop while header_array.to_json.length > 8 * 1024 headers[header_name] = header_array.to_json end def file?(headers) headers['Content-Transfer-Encoding'] == 'binary' || headers['Content-Disposition'] end def sse?(headers) headers['Content-Type'] == 'text/event-stream' end def html_request?(headers, response) headers['Content-Type']&.include?('text/html') end def response_body(response) if response.respond_to?(:body) Array === response.body ? response.body.first : response.body elsif response.respond_to?(:first) response.first end end private def details_attributes <<~EOF id="bullet-footer" data-is-bullet-footer style="cursor: pointer; position: fixed; left: 0px; bottom: 0px; z-index: 9999; background: #fdf2f2; color: #9b1c1c; font-size: 12px; border-radius: 0px 8px 0px 0px; border: 1px solid #9b1c1c;" EOF end def summary_attributes <<~EOF style="font-weight: 600; padding: 2px 8px" EOF end def footer_content_attributes <<~EOF style="padding: 8px; border-top: 1px solid #9b1c1c;" EOF end def footer_console_message if Bullet.console_enabled? "
See 'Uniform Notifier' in JS Console for Stacktrace" end end # Make footer work for XHR requests by appending data to the footer def xhr_script(nonce = nil) script = File.read("#{__dir__}/bullet_xhr.js") if nonce "" else "" end end def with_security_policy_nonce(headers) csp = headers['Content-Security-Policy'] || headers['Content-Security-Policy-Report-Only'] || '' matched = csp.match(NONCE_MATCHER) nonce = matched[:nonce] if matched if nonce console_enabled = UniformNotifier.console alert_enabled = UniformNotifier.alert UniformNotifier.console = { attributes: { nonce: nonce } } if console_enabled UniformNotifier.alert = { attributes: { nonce: nonce } } if alert_enabled yield nonce UniformNotifier.console = console_enabled UniformNotifier.alert = alert_enabled else yield end end end end bullet-7.1.4/lib/bullet/registry.rb000066400000000000000000000004221452565225300172530ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Registry autoload :Base, 'bullet/registry/base' autoload :Object, 'bullet/registry/object' autoload :Association, 'bullet/registry/association' autoload :CallStack, 'bullet/registry/call_stack' end end bullet-7.1.4/lib/bullet/registry/000077500000000000000000000000001452565225300167305ustar00rootroot00000000000000bullet-7.1.4/lib/bullet/registry/association.rb000066400000000000000000000006121452565225300215700ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Registry class Association < Base def merge(base, associations) @registry.merge!(base => associations) end def similarly_associated(base, associations) @registry.select { |key, value| key.include?(base) && value == associations } .collect(&:first).flatten end end end end bullet-7.1.4/lib/bullet/registry/base.rb000066400000000000000000000013471452565225300201740ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Registry class Base attr_reader :registry def initialize @registry = {} end def [](key) @registry[key] end def each(&block) @registry.each(&block) end def delete(base) @registry.delete(base) end def select(*args, &block) @registry.select(*args, &block) end def add(key, value) @registry[key] ||= Set.new if value.is_a? Array @registry[key] += value else @registry[key] << value end end def include?(key, value) !@registry[key].nil? && @registry[key].include?(value) end end end end bullet-7.1.4/lib/bullet/registry/call_stack.rb000066400000000000000000000003511452565225300213540ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Registry class CallStack < Base # remembers found association backtrace def add(key) @registry[key] = Thread.current.backtrace end end end end bullet-7.1.4/lib/bullet/registry/object.rb000066400000000000000000000004471452565225300205300ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Registry class Object < Base def add(bullet_key) super(bullet_key.bullet_class_name, bullet_key) end def include?(bullet_key) super(bullet_key.bullet_class_name, bullet_key) end end end end bullet-7.1.4/lib/bullet/stack_trace_filter.rb000066400000000000000000000040711452565225300212370ustar00rootroot00000000000000# frozen_string_literal: true require "bundler" module Bullet module StackTraceFilter VENDOR_PATH = '/vendor' IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') # @param bullet_key[String] - use this to get stored call stack from call_stacks object. def caller_in_project(bullet_key = nil) vendor_root = Bullet.app_root + VENDOR_PATH bundler_path = Bundler.bundle_path.to_s select_caller_locations(bullet_key) do |location| caller_path = location_as_path(location) caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) && !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) } end end def excluded_stacktrace_path? Bullet.stacktrace_excludes.any? do |exclude_pattern| caller_in_project.any? { |location| pattern_matches?(location, exclude_pattern) } end end private def pattern_matches?(location, pattern) path = location_as_path(location) case pattern when Array pattern_path = pattern.first filter = pattern.last return false unless pattern_matches?(location, pattern_path) case filter when Range filter.include?(location.lineno) when Integer filter == location.lineno when String filter == location.base_label end when String path.include?(pattern) when Regexp path =~ pattern end end def location_as_path(location) return location if location.is_a?(String) IS_RUBY_19 ? location : location.absolute_path.to_s end def select_caller_locations(bullet_key = nil) return caller.select { |caller_path| yield caller_path } if IS_RUBY_19 call_stack = bullet_key ? call_stacks[bullet_key] : caller_locations call_stack.select { |location| yield location } end end end bullet-7.1.4/lib/bullet/version.rb000066400000000000000000000001051452565225300170660ustar00rootroot00000000000000# frozen_string_literal: true module Bullet VERSION = '7.1.4' end bullet-7.1.4/lib/generators/000077500000000000000000000000001452565225300157425ustar00rootroot00000000000000bullet-7.1.4/lib/generators/bullet/000077500000000000000000000000001452565225300172315ustar00rootroot00000000000000bullet-7.1.4/lib/generators/bullet/install_generator.rb000066400000000000000000000023551452565225300232770ustar00rootroot00000000000000# frozen_string_literal: true module Bullet module Generators class InstallGenerator < ::Rails::Generators::Base desc <<~DESC Description: Enable bullet in development/test for your application. DESC def enable_in_development environment(nil, env: 'development') do <<~FILE config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.console = true Bullet.rails_logger = true Bullet.add_footer = true end FILE end say 'Enabled bullet in config/environments/development.rb' end def enable_in_test return unless yes?('Would you like to enable bullet in test environment? (y/n)') environment(nil, env: 'test') do <<~FILE config.after_initialize do Bullet.enable = true Bullet.bullet_logger = true Bullet.raise = true # raise an error if n+1 query occurs end FILE end say 'Enabled bullet in config/environments/test.rb' end end end end bullet-7.1.4/perf/000077500000000000000000000000001452565225300137575ustar00rootroot00000000000000bullet-7.1.4/perf/benchmark.rb000066400000000000000000000063601452565225300162430ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH << 'lib' require 'benchmark' require 'rails' require 'active_record' require 'activerecord-import' require 'bullet' begin require 'perftools' rescue LoadError puts "Could not load perftools.rb, profiling won't be possible" end class Post < ActiveRecord::Base belongs_to :user has_many :comments end class Comment < ActiveRecord::Base belongs_to :user belongs_to :post end class User < ActiveRecord::Base has_many :posts has_many :comments end # create database bullet_benchmark; ActiveRecord::Base.establish_connection( adapter: 'mysql2', database: 'bullet_benchmark', server: '/tmp/mysql.socket', username: 'root' ) ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) } ActiveRecord::Schema.define(version: 1) do create_table :posts do |t| t.column :title, :string t.column :body, :string t.column :user_id, :integer end create_table :comments do |t| t.column :body, :string t.column :post_id, :integer t.column :user_id, :integer end create_table :users do |t| t.column :name, :string end end users_size = 100 posts_size = 1_000 comments_size = 10_000 users = [] users_size.times { |i| users << User.new(name: "user#{i}") } User.import users users = User.all posts = [] posts_size.times { |i| posts << Post.new(title: "Title #{i}", body: "Body #{i}", user: users[i % 100]) } Post.import posts posts = Post.all comments = [] comments_size.times { |i| comments << Comment.new(body: "Comment #{i}", post: posts[i % 1_000], user: users[i % 100]) } Comment.import comments puts 'Start benchmarking...' Bullet.enable = true Benchmark.bm(70) do |bm| bm.report("Querying & Iterating #{posts_size} Posts with #{comments_size} Comments and #{users_size} Users") do 10.times do Bullet.start_request Post.select('SQL_NO_CACHE *').includes(:user, comments: :user).each do |p| p.title p.user.name p.comments.each do |c| c.body c.user.name end end Bullet.end_request end end end puts 'End benchmarking...' # Run benchmark with bundler # # bundle exec ruby perf/benchmark.rb # # bullet 2.3.0 with rails 3.2.2 # user system total real # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 16.460000 0.190000 16.650000 ( 16.968246) # # bullet 2.3.0 with rails 3.1.4 # user system total real # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 14.600000 0.130000 14.730000 ( 14.937590) # # bullet 2.3.0 with rails 3.0.12 # user system total real # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 26.120000 0.430000 26.550000 ( 27.179304) # # # bullet 2.2.1 with rails 3.0.12 # user system total real # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 29.970000 0.270000 30.240000 ( 30.452083) bullet-7.1.4/rails/000077500000000000000000000000001452565225300141355ustar00rootroot00000000000000bullet-7.1.4/rails/init.rb000066400000000000000000000000601452565225300154210ustar00rootroot00000000000000# frozen_string_literal: true require 'bullet' bullet-7.1.4/spec/000077500000000000000000000000001452565225300137555ustar00rootroot00000000000000bullet-7.1.4/spec/bullet/000077500000000000000000000000001452565225300152445ustar00rootroot00000000000000bullet-7.1.4/spec/bullet/detector/000077500000000000000000000000001452565225300170555ustar00rootroot00000000000000bullet-7.1.4/spec/bullet/detector/association_spec.rb000066400000000000000000000014711452565225300227330ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Detector describe Association do before :all do @post1 = Post.first @post2 = Post.last end context '.add_object_association' do it 'should add object, associations pair' do Association.add_object_associations(@post1, :associations) expect(Association.send(:object_associations)).to be_include(@post1.bullet_key, :associations) end end context '.add_call_object_associations' do it 'should add call object, associations pair' do Association.add_call_object_associations(@post1, :associations) expect(Association.send(:call_object_associations)).to be_include(@post1.bullet_key, :associations) end end end end end bullet-7.1.4/spec/bullet/detector/base_spec.rb000066400000000000000000000001751452565225300213310ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Detector describe Base do end end end bullet-7.1.4/spec/bullet/detector/counter_cache_spec.rb000066400000000000000000000041271452565225300232220ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Detector describe CounterCache do before :all do @post1 = Post.first @post2 = Post.last end context '.add_counter_cache' do it 'should create notification if conditions met' do expect(CounterCache).to receive(:conditions_met?).with(@post1, %i[comments]).and_return(true) expect(CounterCache).to receive(:create_notification).with('Post', %i[comments]) CounterCache.add_counter_cache(@post1, %i[comments]) end it 'should not create notification if conditions not met' do expect(CounterCache).to receive(:conditions_met?).with(@post1, %i[comments]).and_return(false) expect(CounterCache).to receive(:create_notification).never CounterCache.add_counter_cache(@post1, %i[comments]) end end context '.add_possible_objects' do it 'should add possible objects' do CounterCache.add_possible_objects([@post1, @post2]) expect(CounterCache.possible_objects).to be_include(@post1.bullet_key) expect(CounterCache.possible_objects).to be_include(@post2.bullet_key) end it 'should add impossible object' do CounterCache.add_impossible_object(@post1) expect(CounterCache.impossible_objects).to be_include(@post1.bullet_key) end end context '.conditions_met?' do it 'should be true when object is possible, not impossible' do CounterCache.add_possible_objects(@post1) expect(CounterCache.conditions_met?(@post1, :associations)).to eq true end it 'should be false when object is not possible' do expect(CounterCache.conditions_met?(@post1, :associations)).to eq false end it 'should be false when object is possible, and impossible' do CounterCache.add_possible_objects(@post1) CounterCache.add_impossible_object(@post1) expect(CounterCache.conditions_met?(@post1, :associations)).to eq false end end end end end bullet-7.1.4/spec/bullet/detector/n_plus_one_query_spec.rb000066400000000000000000000163131452565225300240060ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Detector describe NPlusOneQuery do before(:all) do @post = Post.first @post2 = Post.last end context '.call_association' do it 'should add call_object_associations' do expect(NPlusOneQuery).to receive(:add_call_object_associations).with(@post, :associations) NPlusOneQuery.call_association(@post, :associations) end end context '.possible?' do it 'should be true if possible_objects contain' do NPlusOneQuery.add_possible_objects(@post) expect(NPlusOneQuery.possible?(@post)).to eq true end end context '.impossible?' do it 'should be true if impossible_objects contain' do NPlusOneQuery.add_impossible_object(@post) expect(NPlusOneQuery.impossible?(@post)).to eq true end end context '.association?' do it 'should be true if object, associations pair is already existed' do NPlusOneQuery.add_object_associations(@post, :association) expect(NPlusOneQuery.association?(@post, :association)).to eq true end it 'should be false if object, association pair is not existed' do NPlusOneQuery.add_object_associations(@post, :association1) expect(NPlusOneQuery.association?(@post, :association2)).to eq false end end context '.conditions_met?' do it 'should be true if object is possible, not impossible and object, associations pair is not already existed' do allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true) allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false) allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false) expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq true end it 'should be false if object is not possible, not impossible and object, associations pair is not already existed' do allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(false) allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false) allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false) expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false end it 'should be false if object is possible, but impossible and object, associations pair is not already existed' do allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true) allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(true) allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false) expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false end it 'should be false if object is possible, not impossible and object, associations pair is already existed' do allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true) allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false) allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(true) expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false end end context '.call_association' do it 'should create notification if conditions met' do expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true) expect(NPlusOneQuery).to receive(:caller_in_project).and_return(%w[caller]) expect(NPlusOneQuery).to receive(:create_notification).with(%w[caller], 'Post', :association) NPlusOneQuery.call_association(@post, :association) end it 'should not create notification if conditions not met' do expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(false) expect(NPlusOneQuery).not_to receive(:caller_in_project!) expect(NPlusOneQuery).not_to receive(:create_notification).with('Post', :association) NPlusOneQuery.call_association(@post, :association) end context 'stacktrace_excludes' do before { Bullet.stacktrace_excludes = [/def/] } after { Bullet.stacktrace_excludes = nil } it 'should not create notification when stacktrace contains paths that are in the exclude list' do in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb')) included_path = OpenStruct.new(absolute_path: '/ghi/ghi.rb') excluded_path = OpenStruct.new(absolute_path: '/def/def.rb') expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, included_path, excluded_path]) expect(NPlusOneQuery).to_not receive(:create_notification) NPlusOneQuery.call_association(@post, :association) end # just a sanity spec to make sure the following spec works correctly it "should create notification when stacktrace contains methods that aren't in the exclude list" do method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb')) excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last) expect(NPlusOneQuery).to receive(:caller_locations).at_least(1).and_return([in_project, excluded_path]) expect(NPlusOneQuery).to receive(:conditions_met?).and_return(true) expect(NPlusOneQuery).to receive(:create_notification) NPlusOneQuery.call_association(@post, :association) end it 'should not create notification when stacktrace contains methods that are in the exclude list' do method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location Bullet.stacktrace_excludes = [method] in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb')) excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last) expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, excluded_path]) expect(NPlusOneQuery).to_not receive(:create_notification) NPlusOneQuery.call_association(@post, :association) end end end context '.add_possible_objects' do it 'should add possible objects' do NPlusOneQuery.add_possible_objects([@post, @post2]) expect(NPlusOneQuery.possible_objects).to be_include(@post.bullet_key) expect(NPlusOneQuery.possible_objects).to be_include(@post2.bullet_key) end it 'should not raise error if object is nil' do expect { NPlusOneQuery.add_possible_objects(nil) } .not_to raise_error end end context '.add_impossible_object' do it 'should add impossible object' do NPlusOneQuery.add_impossible_object(@post) expect(NPlusOneQuery.impossible_objects).to be_include(@post.bullet_key) end end end end end bullet-7.1.4/spec/bullet/detector/unused_eager_loading_spec.rb000066400000000000000000000142541452565225300245650ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Detector describe UnusedEagerLoading do before(:all) do @post = Post.first @post2 = Post.all[1] @post3 = Post.last end context '.call_associations' do it 'should get empty array if eager_loadings' do expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to be_empty end it 'should get call associations if object and association are both in eager_loadings and call_object_associations' do UnusedEagerLoading.add_eager_loadings([@post], :association) UnusedEagerLoading.add_call_object_associations(@post, :association) expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq( [:association] ) end it 'should not get call associations if not exist in call_object_associations' do UnusedEagerLoading.add_eager_loadings([@post], :association) expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to be_empty end end context '.diff_object_associations' do it 'should return associations not exist in call_association' do expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq( [:association] ) end it 'should return empty if associations exist in call_association' do UnusedEagerLoading.add_eager_loadings([@post], :association) UnusedEagerLoading.add_call_object_associations(@post, :association) expect( UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association])) ).to be_empty end end context '.check_unused_preload_associations' do let(:paths) { %w[/dir1 /dir1/subdir] } it 'should create notification if object_association_diff is not empty' do UnusedEagerLoading.add_object_associations(@post, :association) allow(UnusedEagerLoading).to receive(:caller_in_project).and_return(paths) expect(UnusedEagerLoading).to receive(:create_notification).with(paths, 'Post', [:association]) UnusedEagerLoading.check_unused_preload_associations end it 'should not create notification if object_association_diff is empty' do UnusedEagerLoading.add_object_associations(@post, :association) UnusedEagerLoading.add_eager_loadings([@post], :association) UnusedEagerLoading.add_call_object_associations(@post, :association) expect( UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association])) ).to be_empty expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association]) UnusedEagerLoading.check_unused_preload_associations end it 'should create call stack for notification' do UnusedEagerLoading.add_object_associations(@post, :association) expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty end end context '.add_eager_loadings' do it 'should add objects, associations pair when eager_loadings are empty' do UnusedEagerLoading.add_eager_loadings([@post, @post2], :associations) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include( [@post.bullet_key, @post2.bullet_key], :associations ) end it 'should add objects, associations pair for existing eager_loadings' do UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1) UnusedEagerLoading.add_eager_loadings([@post, @post2], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include( [@post.bullet_key, @post2.bullet_key], :association1 ) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include( [@post.bullet_key, @post2.bullet_key], :association2 ) end it 'should merge objects, associations pair for existing eager_loadings' do UnusedEagerLoading.add_eager_loadings([@post], :association1) UnusedEagerLoading.add_eager_loadings([@post, @post2], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association2) end it 'should vmerge objects recursively, associations pair for existing eager_loadings' do UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1) UnusedEagerLoading.add_eager_loadings([@post, @post3], :association1) UnusedEagerLoading.add_eager_loadings([@post, @post3], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association1) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post3.bullet_key], :association1) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post3.bullet_key], :association2) end it 'should delete objects, associations pair for existing eager_loadings' do UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1) UnusedEagerLoading.add_eager_loadings([@post], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2) expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association1) end end end end end bullet-7.1.4/spec/bullet/ext/000077500000000000000000000000001452565225300160445ustar00rootroot00000000000000bullet-7.1.4/spec/bullet/ext/object_spec.rb000066400000000000000000000030211452565225300206450ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe Object do context 'bullet_key' do it 'should return class and id composition' do post = Post.first expect(post.bullet_key).to eq("Post:#{post.id}") end if mongoid? it 'should return class with namespace and id composition' do post = Mongoid::Post.first expect(post.bullet_key).to eq("Mongoid::Post:#{post.id}") end end end context 'bullet_primary_key_value' do it 'should return id' do post = Post.first expect(post.bullet_primary_key_value).to eq(post.id) end it 'should return primary key value' do post = Post.first Post.primary_key = 'name' expect(post.bullet_primary_key_value).to eq(post.name) Post.primary_key = 'id' end it 'should return value for multiple primary keys from the composite_primary_key gem' do post = Post.first allow(Post).to receive(:primary_keys).and_return(%i[category_id writer_id]) expect(post.bullet_primary_key_value).to eq("#{post.category_id},#{post.writer_id}") end it 'should return value for multiple primary keys from ActiveRecord 7.1' do post = Post.first allow(Post).to receive(:primary_key).and_return(%i[category_id writer_id]) expect(post.bullet_primary_key_value).to eq("#{post.category_id},#{post.writer_id}") end it 'it should return nil for unpersisted records' do post = Post.new(id: 123) expect(post.bullet_primary_key_value).to be_nil end end end bullet-7.1.4/spec/bullet/ext/string_spec.rb000066400000000000000000000005541452565225300207150ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe String do context 'bullet_class_name' do it 'should only return class name' do expect('Post:1'.bullet_class_name).to eq('Post') end it 'should return class name with namespace' do expect('Mongoid::Post:1234567890'.bullet_class_name).to eq('Mongoid::Post') end end end bullet-7.1.4/spec/bullet/notification/000077500000000000000000000000001452565225300177325ustar00rootroot00000000000000bullet-7.1.4/spec/bullet/notification/base_spec.rb000066400000000000000000000061641452565225300222120ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Notification describe Base do subject { Base.new(Post, %i[comments votes]) } context '#title' do it 'should raise NoMethodError' do expect { subject.title } .to raise_error(NoMethodError) end end context '#body' do it 'should raise NoMethodError' do expect { subject.body } .to raise_error(NoMethodError) end end context '#whoami' do it 'should display user name' do user = `whoami`.chomp expect(subject.whoami).to eq("user: #{user}") end it 'should leverage ENV parameter' do temp_env_variable('USER', 'bogus') { expect(subject.whoami).to eq('user: bogus') } end it 'should return blank if no user available' do temp_env_variable('USER', '') do expect(subject).to receive(:`).with('whoami').and_return('') expect(subject.whoami).to eq('') end end it 'should return blank if whoami is not available' do temp_env_variable('USER', '') do expect(subject).to receive(:`).with('whoami').and_raise(Errno::ENOENT) expect(subject.whoami).to eq('') end end def temp_env_variable(name, value) old_value = ENV[name] ENV[name] = value yield ensure ENV[name] = old_value end end context '#body_with_caller' do it 'should return body' do allow(subject).to receive(:body).and_return('body') allow(subject).to receive(:call_stack_messages).and_return('call_stack_messages') expect(subject.body_with_caller).to eq("body\ncall_stack_messages\n") end end context '#notification_data' do it 'should return notification data' do allow(subject).to receive(:whoami).and_return('whoami') allow(subject).to receive(:url).and_return('url') allow(subject).to receive(:title).and_return('title') allow(subject).to receive(:body_with_caller).and_return('body_with_caller') expect(subject.notification_data).to eq(user: 'whoami', url: 'url', title: 'title', body: 'body_with_caller') end end context '#notify_inline' do it 'should send full_notice to notifier' do notifier = double allow(subject).to receive(:notifier).and_return(notifier) allow(subject).to receive(:notification_data).and_return({ foo: :bar }) expect(notifier).to receive(:inline_notify).with({ foo: :bar }) subject.notify_inline end end context '#notify_out_of_channel' do it 'should send full_out_of_channel to notifier' do notifier = double allow(subject).to receive(:notifier).and_return(notifier) allow(subject).to receive(:notification_data).and_return({ foo: :bar }) expect(notifier).to receive(:out_of_channel_notify).with({ foo: :bar }) subject.notify_out_of_channel end end end end end bullet-7.1.4/spec/bullet/notification/counter_cache_spec.rb000066400000000000000000000005471452565225300241010ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Notification describe CounterCache do subject { CounterCache.new(Post, %i[comments votes]) } it { expect(subject.body).to eq(' Post => [:comments, :votes]') } it { expect(subject.title).to eq('Need Counter Cache with Active Record size') } end end end bullet-7.1.4/spec/bullet/notification/n_plus_one_query_spec.rb000066400000000000000000000020371452565225300246610ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Notification describe NPlusOneQuery do subject { NPlusOneQuery.new([%w[caller1 caller2]], Post, %i[comments votes], 'path') } it do expect(subject.body_with_caller).to eq( " Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])\nCall stack\n caller1\n caller2\n" ) end it do expect([subject.body_with_caller, subject.body_with_caller]).to eq( [ " Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])\nCall stack\n caller1\n caller2\n", " Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])\nCall stack\n caller1\n caller2\n" ] ) end it do expect(subject.body).to eq(" Post => [:comments, :votes]\n Add to your query: .includes([:comments, :votes])") end it { expect(subject.title).to eq('USE eager loading in path') } end end end bullet-7.1.4/spec/bullet/notification/unused_eager_loading_spec.rb000066400000000000000000000007211452565225300254340ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Notification describe UnusedEagerLoading do subject { UnusedEagerLoading.new([''], Post, %i[comments votes], 'path') } it do expect(subject.body).to eq( " Post => [:comments, :votes]\n Remove from your query: .includes([:comments, :votes])" ) end it { expect(subject.title).to eq('AVOID eager loading in path') } end end end bullet-7.1.4/spec/bullet/notification_collector_spec.rb000066400000000000000000000014651452565225300233450ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet describe NotificationCollector do subject { NotificationCollector.new.tap { |collector| collector.add('value') } } context '#add' do it 'should add a value' do subject.add('value1') expect(subject.collection).to be_include('value1') end end context '#reset' do it 'should reset collector' do subject.reset expect(subject.collection).to be_empty end end context '#notifications_present?' do it 'should be true if collection is not empty' do expect(subject).to be_notifications_present end it 'should be false if collection is empty' do subject.reset expect(subject).not_to be_notifications_present end end end end bullet-7.1.4/spec/bullet/rack_spec.rb000066400000000000000000000360141452565225300175270ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet describe Rack do let(:middleware) { Bullet::Rack.new app } let(:app) { Support::AppDouble.new } context '#html_request?' do it 'should be true if Content-Type is text/html and http body contains html tag' do headers = { 'Content-Type' => 'text/html' } response = double(body: '') expect(middleware).to be_html_request(headers, response) end it 'should be true if Content-Type is text/html and http body contains html tag with attributes' do headers = { 'Content-Type' => 'text/html' } response = double(body: "") expect(middleware).to be_html_request(headers, response) end it 'should be false if there is no Content-Type header' do headers = {} response = double(body: '') expect(middleware).not_to be_html_request(headers, response) end it 'should be false if Content-Type is javascript' do headers = { 'Content-Type' => 'text/javascript' } response = double(body: '') expect(middleware).not_to be_html_request(headers, response) end end context 'empty?' do it 'should be false if response is a string and not empty' do response = double(body: '') expect(middleware).not_to be_empty(response) end it 'should be false if response is not found' do response = ['Not Found'] expect(middleware).not_to be_empty(response) end it 'should be true if response body is empty' do response = double(body: '') expect(middleware).to be_empty(response) end it 'should be true if no response body' do response = double expect(middleware).to be_empty(response) end end context '#call' do context 'when Bullet is enabled' do it 'should return original response body' do expected_response = Support::ResponseDouble.new 'Actual body' app.response = expected_response _, _, response = middleware.call({}) expect(response).to eq(expected_response) end it 'should change response body if notification is active' do expect(Bullet).to receive(:notification?).and_return(true) expect(Bullet).to receive(:console_enabled?).and_return(true) expect(Bullet).to receive(:gather_inline_notifications).and_return('') expect(Bullet).to receive(:perform_out_of_channel_notifications) _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq('56') expect(response).to eq(%w[]) end it 'should change response body if always_append_html_body is true' do expect(Bullet).to receive(:always_append_html_body).and_return(true) expect(Bullet).to receive(:console_enabled?).and_return(true) expect(Bullet).to receive(:gather_inline_notifications).and_return('') expect(Bullet).to receive(:perform_out_of_channel_notifications) _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq('56') expect(response).to eq(%w[]) end it 'should set the right Content-Length if response body contains accents' do response = Support::ResponseDouble.new response.body = 'é' app.response = response expect(Bullet).to receive(:notification?).and_return(true) allow(Bullet).to receive(:console_enabled?).and_return(true) expect(Bullet).to receive(:gather_inline_notifications).and_return('') _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq('58') end shared_examples 'inject notifiers' do before do allow(Bullet).to receive(:gather_inline_notifications).and_return('') allow(middleware).to receive(:xhr_script).and_return('') allow(middleware).to receive(:footer_note).and_return('footer') expect(Bullet).to receive(:perform_out_of_channel_notifications) end it 'should change response body if add_footer is true' do expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true) _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s) expect(response).to eq(%w[footer]) end it 'should change response body for html safe string if add_footer is true' do expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true) app.response = Support::ResponseDouble.new.tap do |response| response.body = ActiveSupport::SafeBuffer.new('') end _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s) expect(response).to eq(%w[footer]) end it 'should add the footer-text header for non-html requests when add_footer is true' do allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true) allow(Bullet).to receive(:footer_info).and_return(['footer text']) app.headers = { 'Content-Type' => 'application/json' } _, headers, _response = middleware.call({}) expect(headers).to include('X-bullet-footer-text' => '["footer text"]') end it 'should change response body if console_enabled is true' do expect(Bullet).to receive(:console_enabled?).and_return(true) _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq('56') expect(response).to eq(%w[]) end it 'should include CSP nonce in inline script if console_enabled and a CSP is applied' do allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true) expect(Bullet).to receive(:console_enabled?).and_return(true) allow(middleware).to receive(:xhr_script).and_call_original nonce = '+t9/wTlgG6xbHxXYUaDNzQ==' app.headers = { 'Content-Type' => 'text/html', 'Content-Security-Policy' => "default-src 'self' https:; script-src 'self' https: 'nonce-#{nonce}'" } _, headers, response = middleware.call('Content-Type' => 'text/html') size = 56 + middleware.send(:footer_note).length + middleware.send(:xhr_script, nonce).length expect(headers['Content-Length']).to eq(size.to_s) end it 'should include CSP nonce in inline script if console_enabled and a CSP (report only) is applied' do allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true) expect(Bullet).to receive(:console_enabled?).and_return(true) allow(middleware).to receive(:xhr_script).and_call_original nonce = '+t9/wTlgG6xbHxXYUaDNzQ==' app.headers = { 'Content-Type' => 'text/html', 'Content-Security-Policy-Report-Only' => "default-src 'self' https:; script-src 'self' https: 'nonce-#{nonce}'" } _, headers, response = middleware.call('Content-Type' => 'text/html') size = 56 + middleware.send(:footer_note).length + middleware.send(:xhr_script, nonce).length expect(headers['Content-Length']).to eq(size.to_s) end it 'should change response body for html safe string if console_enabled is true' do expect(Bullet).to receive(:console_enabled?).and_return(true) app.response = Support::ResponseDouble.new.tap do |response| response.body = ActiveSupport::SafeBuffer.new('') end _, headers, response = middleware.call('Content-Type' => 'text/html') expect(headers['Content-Length']).to eq('56') expect(response).to eq(%w[]) end it 'should add headers for non-html requests when console_enabled is true' do allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true) allow(Bullet).to receive(:text_notifications).and_return(['text notifications']) app.headers = { 'Content-Type' => 'application/json' } _, headers, _response = middleware.call({}) expect(headers).to include('X-bullet-console-text' => '["text notifications"]') end it "shouldn't change response body unnecessarily" do expected_response = Support::ResponseDouble.new 'Actual body' app.response = expected_response _, _, response = middleware.call({}) expect(response).to eq(expected_response) end it "shouldn't add headers unnecessarily" do app.headers = { 'Content-Type' => 'application/json' } _, headers, _response = middleware.call({}) expect(headers).not_to include('X-bullet-footer-text') expect(headers).not_to include('X-bullet-console-text') end context 'when skip_http_headers is enabled' do before do allow(Bullet).to receive(:skip_http_headers).and_return(true) end it 'should include the footer but not the xhr script tag if add_footer is true' do expect(Bullet).to receive(:add_footer).at_least(:once).and_return(true) _, headers, response = middleware.call({}) expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s) expect(response).to eq(%w[footer]) end it 'should not include the xhr script tag if console_enabled is true' do expect(Bullet).to receive(:console_enabled?).and_return(true) _, headers, response = middleware.call({}) expect(headers['Content-Length']).to eq('56') expect(response).to eq(%w[]) end it 'should not add the footer-text header for non-html requests when add_footer is true' do allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true) app.headers = { 'Content-Type' => 'application/json' } _, headers, _response = middleware.call({}) expect(headers).not_to include('X-bullet-footer-text') end it 'should not add headers for non-html requests when console_enabled is true' do allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true) app.headers = { 'Content-Type' => 'application/json' } _, headers, _response = middleware.call({}) expect(headers).not_to include('X-bullet-console-text') end end end context 'with notifications present' do before do expect(Bullet).to receive(:notification?).and_return(true) end include_examples 'inject notifiers' end context 'with always_append_html_body true' do before do expect(Bullet).to receive(:always_append_html_body).and_return(true) end include_examples 'inject notifiers' end context 'when skip_html_injection is enabled' do it 'should not try to inject html' do expected_response = Support::ResponseDouble.new 'Actual body' app.response = expected_response allow(Bullet).to receive(:notification?).and_return(true) allow(Bullet).to receive(:skip_html_injection?).and_return(true) expect(Bullet).to receive(:gather_inline_notifications).never expect(middleware).to receive(:xhr_script).never expect(Bullet).to receive(:perform_out_of_channel_notifications) _, _, response = middleware.call('Content-Type' => 'text/html') expect(response).to eq(expected_response) end end end context 'when Bullet is disabled' do before(:each) { allow(Bullet).to receive(:enable?).and_return(false) } it 'should not call Bullet.start_request' do expect(Bullet).not_to receive(:start_request) middleware.call({}) end end end context '#set_header' do it 'should truncate headers to under 8kb' do long_header = ['a' * 1_024] * 10 expected_res = (['a' * 1_024] * 7).to_json expect(middleware.set_header({}, 'Dummy-Header', long_header)).to eq(expected_res) end end describe '#response_body' do let(:response) { double } let(:body_string) { 'My Body' } context 'when `response` responds to `body`' do before { allow(response).to receive(:body).and_return(body) } context 'when `body` returns an Array' do let(:body) { [body_string, 'random string'] } it 'should return the plain body string' do expect(middleware.response_body(response)).to eq body_string end end context 'when `body` does not return an Array' do let(:body) { body_string } it 'should return the plain body string' do expect(middleware.response_body(response)).to eq body_string end end end context 'when `response` does not respond to `body`' do before { allow(response).to receive(:first).and_return(body_string) } it 'should return the plain body string' do expect(middleware.response_body(response)).to eq body_string end end begin require 'rack/files' context 'when `response` is a Rack::Files::Iterator' do let(:response) { instance_double(::Rack::Files::Iterator) } before { allow(response).to receive(:is_a?).with(::Rack::Files::Iterator) { true } } it 'should return nil' do expect(middleware.response_body(response)).to be_nil end end rescue LoadError end end end end bullet-7.1.4/spec/bullet/registry/000077500000000000000000000000001452565225300171145ustar00rootroot00000000000000bullet-7.1.4/spec/bullet/registry/association_spec.rb000066400000000000000000000014261452565225300227720ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Registry describe Association do subject { Association.new.tap { |association| association.add(%w[key1 key2], 'value') } } context '#merge' do it 'should merge key/value' do subject.merge('key0', 'value0') expect(subject['key0']).to be_include('value0') end end context '#similarly_associated' do it 'should return similarly associated keys' do expect(subject.similarly_associated('key1', Set.new(%w[value]))).to eq(%w[key1 key2]) end it 'should return empty if key does not exist' do expect(subject.similarly_associated('key3', Set.new(%w[value]))).to be_empty end end end end end bullet-7.1.4/spec/bullet/registry/base_spec.rb000066400000000000000000000022051452565225300213640ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Registry describe Base do subject { Base.new.tap { |base| base.add('key', 'value') } } context '#[]' do it 'should get value by key' do expect(subject['key']).to eq(Set.new(%w[value])) end end context '#delete' do it 'should delete key' do subject.delete('key') expect(subject['key']).to be_nil end end context '#add' do it 'should add value with string' do subject.add('key', 'new_value') expect(subject['key']).to eq(Set.new(%w[value new_value])) end it 'should add value with array' do subject.add('key', %w[value1 value2]) expect(subject['key']).to eq(Set.new(%w[value value1 value2])) end end context '#include?' do it 'should include key/value' do expect(subject.include?('key', 'value')).to eq true end it 'should not include wrong key/value' do expect(subject.include?('key', 'val')).to eq false end end end end end bullet-7.1.4/spec/bullet/registry/object_spec.rb000066400000000000000000000011531452565225300217210ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet module Registry describe Object do let(:post) { Post.first } let(:another_post) { Post.last } subject { Object.new.tap { |object| object.add(post.bullet_key) } } context '#include?' do it 'should include the object' do expect(subject).to be_include(post.bullet_key) end end context '#add' do it 'should add an object' do subject.add(another_post.bullet_key) expect(subject).to be_include(another_post.bullet_key) end end end end end bullet-7.1.4/spec/bullet/stack_trace_filter_spec.rb000066400000000000000000000014011452565225300224270ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' module Bullet RSpec.describe StackTraceFilter do let(:dummy_class) { Class.new { extend StackTraceFilter } } let(:root_path) { Dir.pwd } let(:bundler_path) { Bundler.bundle_path } describe '#caller_in_project' do it 'gets the caller in the project' do expect(dummy_class).to receive(:call_stacks).and_return( { 'Post:1' => [ File.join(root_path, 'lib/bullet.rb'), File.join(root_path, 'vendor/uniform_notifier.rb'), File.join(bundler_path, 'rack.rb') ] } ) expect(dummy_class.caller_in_project('Post:1')).to eq([File.join(root_path, 'lib/bullet.rb')]) end end end end bullet-7.1.4/spec/bullet_spec.rb000066400000000000000000000123351452565225300166070ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe Bullet, focused: true do subject { Bullet } describe '#enable' do context 'enable Bullet' do before do # Bullet.enable # Do nothing. Bullet has already been enabled for the whole test suite. end it 'should be enabled' do expect(subject).to be_enable end context 'disable Bullet' do before { Bullet.enable = false } it 'should be disabled' do expect(subject).to_not be_enable end context 'enable Bullet again without patching again the orms' do before do expect(Bullet::Mongoid).not_to receive(:enable) if defined?(Bullet::Mongoid) expect(Bullet::ActiveRecord).not_to receive(:enable) if defined?(Bullet::ActiveRecord) Bullet.enable = true end it 'should be enabled again' do expect(subject).to be_enable end end end end end # Testing the aliases. describe '#enabled' do context 'enable Bullet' do before do # Bullet.enable # Do nothing. Bullet has already been enabled for the whole test suite. end it 'should be enabled' do expect(subject).to be_enabled end context 'disable Bullet' do before { Bullet.enabled = false } it 'should be disabled' do expect(subject).to_not be_enabled end context 'enable Bullet again without patching again the orms' do before do expect(Bullet::Mongoid).not_to receive(:enabled) if defined?(Bullet::Mongoid) expect(Bullet::ActiveRecord).not_to receive(:enabled) if defined?(Bullet::ActiveRecord) Bullet.enabled = true end it 'should be enabled again' do expect(subject).to be_enabled end end end end end describe '#start?' do context 'when bullet is disabled' do before(:each) { Bullet.enable = false } it 'should not be started' do expect(Bullet).not_to be_start end end end describe '#debug' do before(:each) { $stdout = StringIO.new } after(:each) { $stdout = STDOUT } context 'when debug is enabled' do before(:each) { ENV['BULLET_DEBUG'] = 'true' } after(:each) { ENV['BULLET_DEBUG'] = 'false' } it 'should output debug information' do Bullet.debug('debug_message', 'this is helpful information') expect($stdout.string).to eq("[Bullet][debug_message] this is helpful information\n") end end context 'when debug is disabled' do it 'should output debug information' do Bullet.debug('debug_message', 'this is helpful information') expect($stdout.string).to be_empty end end end describe '#add_safelist' do context "for 'special' class names" do it 'is added to the safelist successfully' do Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department) expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department end end end describe '#delete_safelist' do context "for 'special' class names" do it 'is deleted from the safelist successfully' do Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department) Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department) expect(Bullet.safelist[:n_plus_one_query]).to eq({}) end end context 'when exists multiple definitions' do it 'is deleted from the safelist successfully' do Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department) Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team) Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team) expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team end end end describe '#perform_out_of_channel_notifications' do let(:notification) { double } before do allow(Bullet).to receive(:for_each_active_notifier_with_notification).and_yield(notification) allow(notification).to receive(:notify_out_of_channel) end context 'when called with Rack environment hash' do let(:env) { { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/path', 'QUERY_STRING' => 'foo=bar' } } context "when env['REQUEST_URI'] is nil" do before { env['REQUEST_URI'] = nil } it 'should notification.url is built' do expect(notification).to receive(:url=).with('GET /path?foo=bar') Bullet.perform_out_of_channel_notifications(env) end end context "when env['REQUEST_URI'] is present" do before { env['REQUEST_URI'] = 'http://example.com/path' } it "should notification.url is env['REQUEST_URI']" do expect(notification).to receive(:url=).with('GET http://example.com/path') Bullet.perform_out_of_channel_notifications(env) end end end end end bullet-7.1.4/spec/integration/000077500000000000000000000000001452565225300163005ustar00rootroot00000000000000bullet-7.1.4/spec/integration/active_record/000077500000000000000000000000001452565225300211115ustar00rootroot00000000000000bullet-7.1.4/spec/integration/active_record/association_spec.rb000066400000000000000000001150361452565225300247720ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' if active_record? describe Bullet::Detector::Association, 'has_many' do context 'post => comments' do it 'should detect non preload post => comments' do Post.all.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end it 'should detect non preload post => comments for find_by_sql' do Post.find_by_sql('SELECT * FROM posts').each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end it 'should detect preload with post => comments' do Post.includes(:comments).each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload post => comments' do Post.includes(:comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect unused preload post => comments' do Post.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preload comment => post with inverse_of' do Post.includes(:comments).each do |post| post.comments.each do |comment| comment.name comment.post.name end end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preload comment => post with inverse_of from a query' do Post.first.comments.find_each do |comment| comment.name comment.post.name end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Post.first.comments.count).not_to eq(0) expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preload post => comments with empty?' do Post.all.each { |post| post.comments.empty? } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end it 'should detect non preload post => comments with include?' do comment = Comment.last Post.all.each { |post| post.comments.include?(comment) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end if ActiveRecord::VERSION::MAJOR != 4 && ActiveRecord::VERSION::MINOR != 0 it 'should not detect unused preload post => comment with empty?' do Post.includes(:comments).each { |post| post.comments.empty? } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end it 'should not detect unused preload post => comment with count' do Post.includes(:comments).each { |post| post.comments.count } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preload post => comments with count' do Post.all.each { |post| post.comments.count } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end end context 'category => posts => comments' do it 'should detect non preload category => posts => comments' do Category.all.each { |category| category.posts.each { |post| post.comments.map(&:name) } } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Category, :posts) expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end it 'should detect preload category => posts, but no post => comments' do Category.includes(:posts).each { |category| category.posts.each { |post| post.comments.map(&:name) } } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).not_to be_detecting_unpreloaded_association_for(Category, :posts) expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) end it 'should detect preload with category => posts => comments' do Category.includes(posts: :comments).each { |category| category.posts.each { |post| post.comments.map(&:name) } } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect preload with category => posts => comments with posts.id > 0' do Category.includes(posts: :comments).where('posts.id > 0').references(:posts).each do |category| category.posts.each { |post| post.comments.map(&:name) } end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with category => posts => comments' do Category.includes(posts: :comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with post => comments, no category => posts' do Category.includes(posts: :comments).each { |category| category.posts.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'category => posts, category => entries' do it 'should detect non preload with category => [posts, entries]' do Category.all.each do |category| category.posts.map(&:name) category.entries.map(&:name) end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Category, :posts) expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Category, :entries) end it 'should detect preload with category => posts, but not with category => entries' do Category.includes(:posts).each do |category| category.posts.map(&:name) category.entries.map(&:name) end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).not_to be_detecting_unpreloaded_association_for(Category, :posts) expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Category, :entries) end it 'should detect preload with category => [posts, entries]' do Category.includes(%i[posts entries]).each do |category| category.posts.map(&:name) category.entries.map(&:name) end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with category => [posts, entries]' do Category.includes(%i[posts entries]).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Category, :posts) expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Category, :entries) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with category => entries, but not with category => posts' do Category.includes(%i[posts entries]).each { |category| category.posts.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_unused_preload_associations_for(Category, :posts) expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Category, :entries) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'post => comment' do it 'should detect unused preload with post => comments' do Post.includes(:comments).each { |post| post.comments.first&.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_unused_preload_associations_for(Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect preload with post => comments' do Post.first.comments.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect unused preload with category => posts' do category = Category.first category.draft_post.destroy! post = category.draft_post post.update!(link: true) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations Support::SqliteSeed.setup_db Support::SqliteSeed.seed_db end end context 'category => posts => writer' do it 'should not detect unused preload associations' do category = Category.includes(posts: :writer).order('id DESC').find_by_name('first') category.posts.map do |post| post.name post.writer.name end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_unused_preload_associations_for(Category, :posts) expect(Bullet::Detector::Association).not_to be_unused_preload_associations_for(Post, :writer) end end context 'scope for_category_name' do it 'should detect preload with post => category' do Post.in_category_name('first').references(:categories).each { |post| post.category.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not be unused preload post => category' do Post.in_category_name('first').references(:categories).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'scope preload_comments' do it 'should detect preload post => comments with scope' do Post.preload_comments.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with scope' do Post.preload_comments.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end describe Bullet::Detector::Association, 'belongs_to' do context 'comment => post' do it 'should detect non preload with comment => post' do Comment.all.each { |comment| comment.post.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Comment, :post) end it 'should detect preload with one comment => post' do Comment.first.post.name Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect preload with comment => post' do Comment.includes(:post).each { |comment| comment.post.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload with comment => post' do Comment.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with comment => post' do Comment.includes(:post).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Comment, :post) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect newly assigned object in an after_save' do new_post = Post.new(category: Category.first) new_post.trigger_after_save = true new_post.save! Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'comment => post => category' do it 'should detect non preload association with comment => post' do Comment.all.each { |comment| comment.post.category.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Comment, :post) end it 'should not detect non preload association with only one comment' do Comment.first.post.category.name Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preload association with post => category' do Comment.includes(:post).each { |comment| comment.post.category.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :category) end it 'should not detect unpreload association' do Comment.includes(post: :category).each { |comment| comment.post.category.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'comment => author, post => writer' do it 'should detect non preloaded writer' do Comment.includes(%i[author post]).where(['base_users.id = ?', BaseUser.first]).references(:base_users) .each { |comment| comment.post.writer.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :writer) end it 'should detect unused preload with comment => author' do Comment.includes([:author, { post: :writer }]).where(['base_users.id = ?', BaseUser.first]).references( :base_users ).each { |comment| comment.post.writer.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preloading with writer => newspaper' do Comment.includes(post: :writer).where("posts.name like '%first%'").references(:posts).each do |comment| comment.post.writer.newspaper.name end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Writer, :newspaper) end it 'should not raise a stack error from posts to category' do expect { Comment.includes(post: :category).each { |com| com.post.category } } .not_to raise_error end end end describe Bullet::Detector::Association, 'has_and_belongs_to_many' do context 'posts <=> deals' do it 'should detect preload associations with join tables that have identifier' do Post.includes(:deals).each { |post| post.deals.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'students <=> teachers' do it 'should detect non preload associations' do Student.all.each { |student| student.teachers.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Student, :teachers) end it 'should detect preload associations' do Student.includes(:teachers).each { |student| student.teachers.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload associations' do Student.includes(:teachers).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Student, :teachers) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect no unused preload associations' do Student.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect non preload student => teachers with empty?' do Student.all.each { |student| student.teachers.empty? } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Student, :teachers) end end context 'user => roles' do it 'should detect preload associations' do User.first.roles.includes(:resource).each { |role| role.resource } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end describe Bullet::Detector::Association, 'has_many :through' do context 'firm => clients' do it 'should detect non preload associations' do Firm.all.each { |firm| firm.clients.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Firm, :clients) end it 'should detect preload associations' do Firm.preload(:clients).each { |firm| firm.clients.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect eager load association' do Firm.eager_load(:clients).each { |firm| firm.clients.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload associations' do Firm.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload associations' do Firm.includes(:clients).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Firm, :clients) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'firm => clients => groups' do it 'should detect non preload associations' do Firm.all.each { |firm| firm.groups.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Firm, :groups) end it 'should detect preload associations' do Firm.preload(:groups).each { |firm| firm.groups.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect eager load associations' do Firm.eager_load(:groups).each { |firm| firm.groups.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload associations' do Firm.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload associations' do Firm.includes(:groups).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Firm, :groups) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end describe Bullet::Detector::Association, 'has_one' do context 'company => address' do it 'should detect non preload association' do Company.all.each { |company| company.address.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Company, :address) end it 'should detect preload association' do Company.includes(:address).each { |company| company.address.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload association' do Company.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload association' do Company.includes(:address).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Company, :address) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end describe Bullet::Detector::Association, 'has_one => has_many' do it 'should not detect preload association' do user = User.first user.submission.replies.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end describe Bullet::Detector::Association, 'has_one :through' do context 'user => attachment' do it 'should detect non preload associations' do User.all.each { |user| user.submission_attachment.file_name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(User, :submission_attachment) end it 'should not detect preload associations with includes' do User.includes(:submission_attachment).each { |user| user.submission_attachment.file_name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect preload associations with eager_load' do User.eager_load(:submission_attachment).each { |user| user.submission_attachment.file_name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload associations' do User.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload associations' do User.includes(:submission_attachment).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(User, :submission_attachment) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end describe Bullet::Detector::Association, 'call one association that in possible objects' do it 'should not detect preload association' do Post.all Post.first.comments.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end describe Bullet::Detector::Association, 'query immediately after creation' do context 'with save' do context 'document => children' do it 'should not detect non preload associations' do document1 = Document.new document1.children.build document1.save document2 = Document.new(parent: document1) document2.save document2.parent document1.children.each.first Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end context 'with save!' do context 'document => children' do it 'should not detect non preload associations' do document1 = Document.new document1.children.build document1.save! document2 = Document.new(parent: document1) document2.save! document2.parent document1.children.each.first Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end end describe Bullet::Detector::Association, 'STI' do context 'page => author' do it 'should detect non preload associations' do Page.all.each { |page| page.author.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Page, :author) end it 'should detect preload associations' do Page.includes(:author).each { |page| page.author.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload associations' do Page.includes(:author).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Page, :author) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload associations' do Page.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'disable n plus one query' do before { Bullet.n_plus_one_query_enable = false } after { Bullet.n_plus_one_query_enable = true } it 'should not detect n plus one query' do Post.all.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_detecting_unpreloaded_association_for(Post, :comments) expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations end it 'should still detect unused eager loading' do Post.includes(:comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments) end end context 'disable unused eager loading' do before { Bullet.unused_eager_loading_enable = false } after { Bullet.unused_eager_loading_enable = true } it 'should not detect unused eager loading' do Post.includes(:comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations end it 'should still detect n plus one query' do Post.all.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations end end context 'add n plus one query to safelist' do before { Bullet.add_safelist type: :n_plus_one_query, class_name: 'Post', association: :comments } after { Bullet.clear_safelist } it 'should not detect n plus one query' do Post.all.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_detecting_unpreloaded_association_for(Post, :comments) expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations end it 'should still detect unused eager loading' do Post.includes(:comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments) end end context 'add unused eager loading to safelist' do before { Bullet.add_safelist type: :unused_eager_loading, class_name: 'Post', association: :comments } after { Bullet.clear_safelist } it 'should not detect unused eager loading' do Post.includes(:comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations end it 'should still detect n plus one query' do Post.all.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Post, :comments) expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations end end end end bullet-7.1.4/spec/integration/counter_cache_spec.rb000066400000000000000000000063341452565225300224470ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' if !mongoid? && active_record? describe Bullet::Detector::CounterCache do before(:each) { Bullet.start_request } after(:each) { Bullet.end_request } it 'should need counter cache with all cities' do Country.all.each { |country| country.cities.size } expect(Bullet.collected_counter_cache_notifications).not_to be_empty end it 'should not need counter cache if already define counter_cache' do Person.all.each { |person| person.pets.size } expect(Bullet.collected_counter_cache_notifications).to be_empty end it 'should not need counter cache with only one object' do Country.first.cities.size expect(Bullet.collected_counter_cache_notifications).to be_empty end it 'should not need counter cache without size' do Country.includes(:cities).each { |country| country.cities.empty? } expect(Bullet.collected_counter_cache_notifications).to be_empty end if ActiveRecord::VERSION::MAJOR > 4 it 'should not need counter cache for has_many through' do Client.all.each { |client| client.firms.size } expect(Bullet.collected_counter_cache_notifications).to be_empty end else it 'should need counter cache for has_many through' do Client.all.each { |client| client.firms.size } expect(Bullet.collected_counter_cache_notifications).not_to be_empty end end it 'should not need counter cache with part of cities' do Country.all.each { |country| country.cities.where(name: 'first').size } expect(Bullet.collected_counter_cache_notifications).to be_empty end context 'disable' do before { Bullet.counter_cache_enable = false } after { Bullet.counter_cache_enable = true } it 'should not detect counter cache' do Country.all.each { |country| country.cities.size } expect(Bullet.collected_counter_cache_notifications).to be_empty end end context 'safelist' do before { Bullet.add_safelist type: :counter_cache, class_name: 'Country', association: :cities } after { Bullet.clear_safelist } it 'should not detect counter cache' do Country.all.each { |country| country.cities.size } expect(Bullet.collected_counter_cache_notifications).to be_empty end end describe 'with count' do it 'should need counter cache' do Country.all.each { |country| country.cities.count } expect(Bullet.collected_counter_cache_notifications).not_to be_empty end it 'should notify even with counter cache' do Person.all.each { |person| person.pets.count } expect(Bullet.collected_counter_cache_notifications).not_to be_empty end if ActiveRecord::VERSION::MAJOR > 4 it 'should not need counter cache for has_many through' do Client.all.each { |client| client.firms.count } expect(Bullet.collected_counter_cache_notifications).to be_empty end else it 'should need counter cache for has_many through' do Client.all.each { |client| client.firms.count } expect(Bullet.collected_counter_cache_notifications).not_to be_empty end end end end end bullet-7.1.4/spec/integration/mongoid/000077500000000000000000000000001452565225300177345ustar00rootroot00000000000000bullet-7.1.4/spec/integration/mongoid/association_spec.rb000066400000000000000000000265141452565225300236170ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' if mongoid? describe Bullet::Detector::Association do context 'embeds_many' do context 'posts => users' do it 'should detect nothing' do Mongoid::Post.all.each { |post| post.users.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end context 'has_many' do context 'posts => comments' do it 'should detect non preload posts => comments' do Mongoid::Post.all.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Mongoid::Post, :comments) end it 'should detect preload post => comments' do Mongoid::Post.includes(:comments).each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload post => comments' do Mongoid::Post.includes(:comments).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect unused preload post => comments' do Mongoid::Post.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'category => posts, category => entries' do it 'should detect non preload with category => [posts, entries]' do Mongoid::Category.all.each do |category| category.posts.map(&:name) category.entries.map(&:name) end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Mongoid::Category, :posts) expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Mongoid::Category, :entries) end it 'should detect preload with category => posts, but not with category => entries' do Mongoid::Category.includes(:posts).each do |category| category.posts.map(&:name) category.entries.map(&:name) end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).not_to be_detecting_unpreloaded_association_for( Mongoid::Category, :posts ) expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Mongoid::Category, :entries) end it 'should detect preload with category => [posts, entries]' do Mongoid::Category.includes(:posts, :entries).each do |category| category.posts.map(&:name) category.entries.map(&:name) end Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with category => [posts, entries]' do Mongoid::Category.includes(:posts, :entries).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Category, :posts) expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Category, :entries) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with category => entries, but not with category => posts' do Mongoid::Category.includes(:posts, :entries).each { |category| category.posts.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_unused_preload_associations_for(Mongoid::Category, :posts) expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Category, :entries) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'post => comment' do it 'should detect unused preload with post => comments' do Mongoid::Post.includes(:comments).each { |post| post.comments.first.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_unused_preload_associations_for(Mongoid::Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect preload with post => comments' do Mongoid::Post.first.comments.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end context 'scope preload_comments' do it 'should detect preload post => comments with scope' do Mongoid::Post.preload_comments.each { |post| post.comments.map(&:name) } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with scope' do Mongoid::Post.preload_comments.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Post, :comments) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end context 'belongs_to' do context 'comment => post' do it 'should detect non preload with comment => post' do Mongoid::Comment.all.each { |comment| comment.post.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Mongoid::Comment, :post) end it 'should detect preload with one comment => post' do Mongoid::Comment.first.post.name Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect preload with comment => post' do Mongoid::Comment.includes(:post).each { |comment| comment.post.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload with comment => post' do Mongoid::Comment.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload with comments => post' do Mongoid::Comment.includes(:post).map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Comment, :post) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end context 'has_one' do context 'company => address' do if Mongoid::VERSION !~ /\A3.0/ it 'should detect non preload association' do Mongoid::Company.all.each { |company| company.address.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for( Mongoid::Company, :address ) end end it 'should detect preload association' do Mongoid::Company.includes(:address).each { |company| company.address.name } Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should not detect preload association' do Mongoid::Company.all.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end it 'should detect unused preload association' do criteria = Mongoid::Company.includes(:address) criteria.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Mongoid::Company, :address) expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end context 'call one association that in possible objects' do it 'should not detect preload association' do Mongoid::Post.all Mongoid::Post.first.comments.map(&:name) Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations expect(Bullet::Detector::Association).to be_completely_preloading_associations end end end end bullet-7.1.4/spec/models/000077500000000000000000000000001452565225300152405ustar00rootroot00000000000000bullet-7.1.4/spec/models/address.rb000066400000000000000000000001341452565225300172100ustar00rootroot00000000000000# frozen_string_literal: true class Address < ActiveRecord::Base belongs_to :company end bullet-7.1.4/spec/models/attachment.rb000066400000000000000000000001421452565225300177120ustar00rootroot00000000000000# frozen_string_literal: true class Attachment < ActiveRecord::Base belongs_to :submission end bullet-7.1.4/spec/models/author.rb000066400000000000000000000001331452565225300170640ustar00rootroot00000000000000# frozen_string_literal: true class Author < ActiveRecord::Base has_many :documents end bullet-7.1.4/spec/models/base_user.rb000066400000000000000000000002061452565225300175330ustar00rootroot00000000000000# frozen_string_literal: true class BaseUser < ActiveRecord::Base has_many :comments has_many :posts belongs_to :newspaper end bullet-7.1.4/spec/models/category.rb000066400000000000000000000003171452565225300174030ustar00rootroot00000000000000# frozen_string_literal: true class Category < ActiveRecord::Base has_many :posts, inverse_of: :category has_many :entries has_many :users def draft_post posts.draft.first_or_create end end bullet-7.1.4/spec/models/city.rb000066400000000000000000000001311452565225300165300ustar00rootroot00000000000000# frozen_string_literal: true class City < ActiveRecord::Base belongs_to :country end bullet-7.1.4/spec/models/client.rb000066400000000000000000000002371452565225300170450ustar00rootroot00000000000000# frozen_string_literal: true class Client < ActiveRecord::Base belongs_to :group has_many :relationships has_many :firms, through: :relationships end bullet-7.1.4/spec/models/comment.rb000066400000000000000000000003001452565225300172200ustar00rootroot00000000000000# frozen_string_literal: true class Comment < ActiveRecord::Base belongs_to :post, inverse_of: :comments belongs_to :author, class_name: 'BaseUser' validates :post, presence: true end bullet-7.1.4/spec/models/company.rb000066400000000000000000000001311452565225300172260ustar00rootroot00000000000000# frozen_string_literal: true class Company < ActiveRecord::Base has_one :address end bullet-7.1.4/spec/models/country.rb000066400000000000000000000001311452565225300172630ustar00rootroot00000000000000# frozen_string_literal: true class Country < ActiveRecord::Base has_many :cities end bullet-7.1.4/spec/models/deal.rb000066400000000000000000000001441452565225300164710ustar00rootroot00000000000000# frozen_string_literal: true class Deal < ActiveRecord::Base has_and_belongs_to_many :posts end bullet-7.1.4/spec/models/document.rb000066400000000000000000000003521452565225300174030ustar00rootroot00000000000000# frozen_string_literal: true class Document < ActiveRecord::Base has_many :children, class_name: 'Document', foreign_key: 'parent_id' belongs_to :parent, class_name: 'Document', foreign_key: 'parent_id' belongs_to :author end bullet-7.1.4/spec/models/entry.rb000066400000000000000000000001331452565225300167230ustar00rootroot00000000000000# frozen_string_literal: true class Entry < ActiveRecord::Base belongs_to :category end bullet-7.1.4/spec/models/firm.rb000066400000000000000000000002601452565225300165200ustar00rootroot00000000000000# frozen_string_literal: true class Firm < ActiveRecord::Base has_many :relationships has_many :clients, through: :relationships has_many :groups, through: :clients end bullet-7.1.4/spec/models/folder.rb000066400000000000000000000000731452565225300170400ustar00rootroot00000000000000# frozen_string_literal: true class Folder < Document end bullet-7.1.4/spec/models/group.rb000066400000000000000000000001041452565225300167140ustar00rootroot00000000000000# frozen_string_literal: true class Group < ActiveRecord::Base end bullet-7.1.4/spec/models/mongoid/000077500000000000000000000000001452565225300166745ustar00rootroot00000000000000bullet-7.1.4/spec/models/mongoid/address.rb000066400000000000000000000002341452565225300206450ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::Address include Mongoid::Document field :name belongs_to :company, class_name: 'Mongoid::Company' end bullet-7.1.4/spec/models/mongoid/category.rb000066400000000000000000000003101452565225300210300ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::Category include Mongoid::Document field :name has_many :posts, class_name: 'Mongoid::Post' has_many :entries, class_name: 'Mongoid::Entry' end bullet-7.1.4/spec/models/mongoid/comment.rb000066400000000000000000000002261452565225300206630ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::Comment include Mongoid::Document field :name belongs_to :post, class_name: 'Mongoid::Post' end bullet-7.1.4/spec/models/mongoid/company.rb000066400000000000000000000002311452565225300206630ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::Company include Mongoid::Document field :name has_one :address, class_name: 'Mongoid::Address' end bullet-7.1.4/spec/models/mongoid/entry.rb000066400000000000000000000002341452565225300203610ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::Entry include Mongoid::Document field :name belongs_to :category, class_name: 'Mongoid::Category' end bullet-7.1.4/spec/models/mongoid/post.rb000066400000000000000000000004721452565225300202110ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::Post include Mongoid::Document field :name has_many :comments, class_name: 'Mongoid::Comment' belongs_to :category, class_name: 'Mongoid::Category' embeds_many :users, class_name: 'Mongoid::User' scope :preload_comments, -> { includes(:comments) } end bullet-7.1.4/spec/models/mongoid/user.rb000066400000000000000000000001421452565225300201740ustar00rootroot00000000000000# frozen_string_literal: true class Mongoid::User include Mongoid::Document field :name end bullet-7.1.4/spec/models/newspaper.rb000066400000000000000000000001641452565225300175720ustar00rootroot00000000000000# frozen_string_literal: true class Newspaper < ActiveRecord::Base has_many :writers, class_name: 'BaseUser' end bullet-7.1.4/spec/models/page.rb000066400000000000000000000000711452565225300164770ustar00rootroot00000000000000# frozen_string_literal: true class Page < Document end bullet-7.1.4/spec/models/person.rb000066400000000000000000000001261452565225300170720ustar00rootroot00000000000000# frozen_string_literal: true class Person < ActiveRecord::Base has_many :pets end bullet-7.1.4/spec/models/pet.rb000066400000000000000000000001541452565225300163550ustar00rootroot00000000000000# frozen_string_literal: true class Pet < ActiveRecord::Base belongs_to :person, counter_cache: true end bullet-7.1.4/spec/models/post.rb000066400000000000000000000016471452565225300165620ustar00rootroot00000000000000# frozen_string_literal: true class Post < ActiveRecord::Base belongs_to :category, inverse_of: :posts belongs_to :writer has_many :comments, inverse_of: :post has_and_belongs_to_many :deals validates :category, presence: true scope :preload_comments, -> { includes(:comments) } scope :in_category_name, ->(name) { where(['categories.name = ?', name]).includes(:category) } scope :draft, -> { where(active: false) } def link=(*) comments.new end # see association_spec.rb 'should not detect newly assigned object in an after_save' attr_accessor :trigger_after_save after_save do next unless trigger_after_save temp_comment = Comment.new(post: self) # this triggers self to be "possible", even though it's # not saved yet temp_comment.post # category should NOT whine about not being pre-loaded, because # it's obviously attached to a new object category end end bullet-7.1.4/spec/models/relationship.rb000066400000000000000000000001631452565225300202660ustar00rootroot00000000000000# frozen_string_literal: true class Relationship < ActiveRecord::Base belongs_to :firm belongs_to :client end bullet-7.1.4/spec/models/reply.rb000066400000000000000000000001351452565225300167170ustar00rootroot00000000000000# frozen_string_literal: true class Reply < ActiveRecord::Base belongs_to :submission end bullet-7.1.4/spec/models/role.rb000066400000000000000000000002171452565225300165260ustar00rootroot00000000000000# frozen_string_literal: true class Role < ActiveRecord::Base has_and_belongs_to_many :users belongs_to :resource, polymorphic: true end bullet-7.1.4/spec/models/student.rb000066400000000000000000000001521452565225300172510ustar00rootroot00000000000000# frozen_string_literal: true class Student < ActiveRecord::Base has_and_belongs_to_many :teachers end bullet-7.1.4/spec/models/submission.rb000066400000000000000000000002061452565225300177560ustar00rootroot00000000000000# frozen_string_literal: true class Submission < ActiveRecord::Base belongs_to :user has_many :replies has_one :attachment end bullet-7.1.4/spec/models/teacher.rb000066400000000000000000000001521452565225300171760ustar00rootroot00000000000000# frozen_string_literal: true class Teacher < ActiveRecord::Base has_and_belongs_to_many :students end bullet-7.1.4/spec/models/user.rb000066400000000000000000000003671452565225300165510ustar00rootroot00000000000000# frozen_string_literal: true class User < ActiveRecord::Base has_one :submission has_one :submission_attachment, through: :submission, source: :attachment, class_name: 'Attachment' belongs_to :category has_and_belongs_to_many :roles end bullet-7.1.4/spec/models/writer.rb000066400000000000000000000000731452565225300171010ustar00rootroot00000000000000# frozen_string_literal: true class Writer < BaseUser end bullet-7.1.4/spec/spec_helper.rb000066400000000000000000000043461452565225300166020ustar00rootroot00000000000000# frozen_string_literal: true require 'rspec' begin require 'active_record' rescue LoadError end begin require 'mongoid' rescue LoadError end module Rails class << self def root File.expand_path(__FILE__).split('/')[0..-3].join('/') end def env 'test' end end end $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) require 'bullet' extend Bullet::Dependency Bullet.enable = true MODELS = File.join(File.dirname(__FILE__), 'models') $LOAD_PATH.unshift(MODELS) SUPPORT = File.join(File.dirname(__FILE__), 'support') Dir[File.join(SUPPORT, '*.rb')].reject { |filename| filename =~ /_seed.rb$/ } .sort.each { |file| require file } RSpec.configure do |config| config.extend Bullet::Dependency config.filter_run focus: true config.run_all_when_everything_filtered = true end if active_record? ActiveRecord::Migration.verbose = false # Autoload every active_record model for the test suite that sits in spec/models. Dir[File.join(MODELS, '*.rb')].sort.each do |filename| name = File.basename(filename, '.rb') autoload name.camelize.to_sym, name end require File.join(SUPPORT, 'sqlite_seed.rb') RSpec.configure do |config| config.before(:suite) do Support::SqliteSeed.setup_db Support::SqliteSeed.seed_db end config.before(:example) do Bullet.start_request Bullet.enable = true end config.after(:example) { Bullet.end_request } end if ENV['BULLET_LOG'] require 'logger' ActiveRecord::Base.logger = Logger.new(STDOUT) end end if mongoid? # Autoload every mongoid model for the test suite that sits in spec/models. Dir[File.join(MODELS, 'mongoid', '*.rb')].sort.each { |file| require file } require File.join(SUPPORT, 'mongo_seed.rb') RSpec.configure do |config| config.before(:suite) do Support::MongoSeed.setup_db Support::MongoSeed.seed_db end config.after(:suite) do Support::MongoSeed.setup_db Support::MongoSeed.teardown_db end config.before(:each) { Bullet.start_request } config.after(:each) { Bullet.end_request } end if ENV['BULLET_LOG'] Mongoid.logger = Logger.new(STDOUT) Moped.logger = Logger.new(STDOUT) end end bullet-7.1.4/spec/support/000077500000000000000000000000001452565225300154715ustar00rootroot00000000000000bullet-7.1.4/spec/support/bullet_ext.rb000066400000000000000000000040571452565225300201730ustar00rootroot00000000000000# frozen_string_literal: true module Bullet def self.collected_notifications_of_class(notification_class) Bullet.notification_collector.collection.select { |notification| notification.is_a? notification_class } end def self.collected_counter_cache_notifications collected_notifications_of_class Bullet::Notification::CounterCache end def self.collected_n_plus_one_query_notifications collected_notifications_of_class Bullet::Notification::NPlusOneQuery end def self.collected_unused_eager_association_notifications collected_notifications_of_class Bullet::Notification::UnusedEagerLoading end end module Bullet module Detector class Association class << self # returns true if all associations are preloaded def completely_preloading_associations? Bullet.collected_n_plus_one_query_notifications.empty? end def has_unused_preload_associations? Bullet.collected_unused_eager_association_notifications.present? end # returns true if a given object has a specific association def creating_object_association_for?(object, association) object_associations[object.bullet_key].present? && object_associations[object.bullet_key].include?(association) end # returns true if a given class includes the specific unpreloaded association def detecting_unpreloaded_association_for?(klass, association) Bullet.collected_n_plus_one_query_notifications.select do |notification| notification.base_class == klass.to_s && notification.associations.include?(association) end.present? end # returns true if the given class includes the specific unused preloaded association def unused_preload_associations_for?(klass, association) Bullet.collected_unused_eager_association_notifications.select do |notification| notification.base_class == klass.to_s && notification.associations.include?(association) end.present? end end end end end bullet-7.1.4/spec/support/mongo_seed.rb000066400000000000000000000044711452565225300201430ustar00rootroot00000000000000# frozen_string_literal: true module Support module MongoSeed module_function def seed_db category1 = Mongoid::Category.create(name: 'first') category2 = Mongoid::Category.create(name: 'second') post1 = category1.posts.create(name: 'first') post1a = category1.posts.create(name: 'like first') post2 = category2.posts.create(name: 'second') post1.users << Mongoid::User.create(name: 'first') post1.users << Mongoid::User.create(name: 'another') post2.users << Mongoid::User.create(name: 'second') comment1 = post1.comments.create(name: 'first') comment2 = post1.comments.create(name: 'first2') comment3 = post1.comments.create(name: 'first3') comment4 = post1.comments.create(name: 'second') comment8 = post1a.comments.create(name: 'like first 1') comment9 = post1a.comments.create(name: 'like first 2') comment5 = post2.comments.create(name: 'third') comment6 = post2.comments.create(name: 'fourth') comment7 = post2.comments.create(name: 'fourth') entry1 = category1.entries.create(name: 'first') entry2 = category1.entries.create(name: 'second') company1 = Mongoid::Company.create(name: 'first') company2 = Mongoid::Company.create(name: 'second') Mongoid::Address.create(name: 'first', company: company1) Mongoid::Address.create(name: 'second', company: company2) end def setup_db if Mongoid::VERSION =~ /\A4/ Mongoid.configure do |config| config.load_configuration(sessions: { default: { database: 'bullet', hosts: %w[localhost:27017] } }) end else if %w[7.1 7.2 7.3 7.4 7.5 8 8.1].any? { |version| Mongoid::VERSION =~ /\A#{Regexp.quote(version)}/ } Mongoid.logger = Logger.new(STDERR).tap do |logger| logger.level = Logger::WARN end end Mongoid.configure do |config| config.load_configuration(clients: { default: { database: 'bullet', hosts: %w[localhost:27017] } }) end # Increase the level from DEBUG in order to avoid excessive logging to the screen Mongo::Logger.logger.level = Logger::WARN end end def teardown_db Mongoid.purge! Mongoid::IdentityMap.clear if Mongoid.const_defined?(:IdentityMap) end end end bullet-7.1.4/spec/support/rack_double.rb000066400000000000000000000013141452565225300202670ustar00rootroot00000000000000# frozen_string_literal: true module Support class AppDouble def call(_env) env = @env [status, headers, response] end attr_writer :status attr_writer :headers def headers @headers ||= { 'Content-Type' => 'text/html' } @headers end attr_writer :response private def status @status || 200 end def response @response || ResponseDouble.new end end class ResponseDouble def initialize(actual_body = nil) @actual_body = actual_body end def body @body ||= '' end attr_writer :body def each yield body end def close; end end end bullet-7.1.4/spec/support/sqlite_seed.rb000066400000000000000000000211741452565225300203240ustar00rootroot00000000000000# frozen_string_literal: true module Support module SqliteSeed module_function def seed_db newspaper1 = Newspaper.create(name: 'First Newspaper') newspaper2 = Newspaper.create(name: 'Second Newspaper') writer1 = Writer.create(name: 'first', newspaper: newspaper1) writer2 = Writer.create(name: 'second', newspaper: newspaper2) user1 = BaseUser.create(name: 'third', newspaper: newspaper1) user2 = BaseUser.create(name: 'fourth', newspaper: newspaper2) category1 = Category.create(name: 'first') category2 = Category.create(name: 'second') post1 = category1.posts.create(name: 'first', writer: writer1) post1a = category1.posts.create(name: 'like first', writer: writer2, active: false) post2 = category2.posts.create(name: 'second', writer: writer2) post3 = category2.posts.create(name: 'third', writer: writer2) deal1 = Deal.new(name: 'Deal 1') deal1.posts << post1 deal1.posts << post2 deal2 = Deal.new(name: 'Deal 2') post1.deals << deal1 post1.deals << deal2 comment1 = post1.comments.create(name: 'first', author: writer1) comment2 = post1.comments.create(name: 'first2', author: writer1) comment3 = post1.comments.create(name: 'first3', author: writer1) comment4 = post1.comments.create(name: 'second', author: writer2) comment8 = post1a.comments.create(name: 'like first 1', author: writer1) comment9 = post1a.comments.create(name: 'like first 2', author: writer2) comment5 = post2.comments.create(name: 'third', author: user1) comment6 = post2.comments.create(name: 'fourth', author: user2) comment7 = post2.comments.create(name: 'fourth', author: writer1) entry1 = category1.entries.create(name: 'first') entry2 = category1.entries.create(name: 'second') student1 = Student.create(name: 'first') student2 = Student.create(name: 'second') teacher1 = Teacher.create(name: 'first') teacher2 = Teacher.create(name: 'second') student1.teachers = [teacher1, teacher2] student2.teachers = [teacher1, teacher2] teacher1.students << student1 teacher2.students << student2 firm1 = Firm.create(name: 'first') firm2 = Firm.create(name: 'second') group1 = Group.create(name: 'first') group2 = Group.create(name: 'second') client1 = Client.create(name: 'first', group: group1) client2 = Client.create(name: 'second', group: group2) firm1.clients = [client1, client2] firm2.clients = [client1, client2] client1.firms << firm1 client2.firms << firm2 company1 = Company.create(name: 'first') company2 = Company.create(name: 'second') Address.create(name: 'first', company: company1) Address.create(name: 'second', company: company2) country1 = Country.create(name: 'first') country2 = Country.create(name: 'second') country1.cities.create(name: 'first') country1.cities.create(name: 'second') country2.cities.create(name: 'third') country2.cities.create(name: 'fourth') person1 = Person.create(name: 'first') person2 = Person.create(name: 'second') person1.pets.create(name: 'first') person1.pets.create(name: 'second') person2.pets.create(name: 'third') person2.pets.create(name: 'fourth') author1 = Author.create(name: 'author1') author2 = Author.create(name: 'author2') folder1 = Folder.create(name: 'folder1', author_id: author1.id) folder2 = Folder.create(name: 'folder2', author_id: author2.id) page1 = Page.create(name: 'page1', parent_id: folder1.id, author_id: author1.id) page2 = Page.create(name: 'page2', parent_id: folder1.id, author_id: author1.id) page3 = Page.create(name: 'page3', parent_id: folder2.id, author_id: author2.id) page4 = Page.create(name: 'page4', parent_id: folder2.id, author_id: author2.id) role1 = Role.create(name: 'Admin') role2 = Role.create(name: 'User') user1 = User.create(name: 'user1', category: category1) user2 = User.create(name: 'user2', category: category1) user1.roles << role1 user1.roles << role2 user2.roles << role2 submission1 = user1.create_submission(name: 'submission1') submission2 = user2.create_submission(name: 'submission2') submission1.replies.create(name: 'reply1') submission1.replies.create(name: 'reply2') submission2.replies.create(name: 'reply3') submission2.replies.create(name: 'reply4') submission1.create_attachment(file_name: 'submission1 file') submission2.create_attachment(file_name: 'submission2 file') end def setup_db ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') ActiveRecord::Schema.define(version: 1) do create_table :addresses do |t| t.column :name, :string t.column :company_id, :integer end create_table :authors do |t| t.string :name end create_table :base_users do |t| t.column :name, :string t.column :type, :string t.column :newspaper_id, :integer end create_table :categories do |t| t.column :name, :string end create_table :cities do |t| t.string :name t.integer :country_id end create_table :clients do |t| t.column :name, :string t.column :group_id, :integer end create_table :comments do |t| t.column :name, :string t.column :post_id, :integer t.column :author_id, :integer end create_table :companies do |t| t.column :name, :string end create_table :contacts do |t| t.column :name, :string end create_table :countries do |t| t.string :name end create_table :deals do |t| t.column :name, :string t.column :hotel_id, :integer end create_table :deals_posts do |t| t.column :deal_id, :integer t.column :post_id, :integer end create_table :documents do |t| t.string :name t.string :type t.integer :parent_id t.integer :author_id end create_table :emails do |t| t.column :name, :string t.column :contact_id, :integer end create_table :entries do |t| t.column :name, :string t.column :category_id, :integer end create_table :firms do |t| t.column :name, :string end create_table :groups do |t| t.column :name, :string end create_table :hotels do |t| t.column :name, :string t.column :location_id, :integer end create_table :locations do |t| t.column :name, :string end create_table :newspapers do |t| t.column :name, :string end create_table :people do |t| t.string :name t.integer :pets_count end create_table :pets do |t| t.string :name t.integer :person_id end create_table :posts do |t| t.column :name, :string t.column :category_id, :integer t.column :writer_id, :integer t.column :active, :boolean, default: true end create_table :relationships do |t| t.column :firm_id, :integer t.column :client_id, :integer end create_table :students do |t| t.column :name, :string end create_table :students_teachers, id: false do |t| t.column :student_id, :integer t.column :teacher_id, :integer end create_table :teachers do |t| t.column :name, :string end create_table :replies do |t| t.column :name, :string t.column :submission_id, :integer end create_table :roles do |t| t.column :name, :string t.column :resource_id, :integer t.column :resource_type, :string end create_table :roles_users do |t| t.column :role_id, :integer t.column :user_id, :integer end create_table :submissions do |t| t.column :name, :string t.column :user_id, :integer end create_table :users do |t| t.column :name, :string t.column :category_id, :integer end create_table :attachments do |t| t.column :file_name, :string t.column :submission_id, :integer end end end end end bullet-7.1.4/tasks/000077500000000000000000000000001452565225300141505ustar00rootroot00000000000000bullet-7.1.4/tasks/bullet_tasks.rake000066400000000000000000000003351452565225300175110ustar00rootroot00000000000000# frozen_string_literal: true namespace :bullet do namespace :log do desc 'Truncates the bullet log file to zero bytes' task :clear do f = File.open('log/bullet.log', 'w') f.close end end end bullet-7.1.4/test.sh000077500000000000000000000031771452565225300143510ustar00rootroot00000000000000#bundle update rails && bundle exec rspec spec #BUNDLE_GEMFILE=Gemfile.mongoid bundle update mongoid && BUNDLE_GEMFILE=Gemfile.mongoid bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-7.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-7.1 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-7.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-7.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-6.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-6.1 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-6.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-6.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-5.2 bundle && BUNDLE_GEMFILE=Gemfile.rails-5.2 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-5.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-5.1 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-5.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-5.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle && BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.mongoid-8.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-8.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.mongoid-7.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-7.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.mongoid-6.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-6.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle exec rspec spec BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle && BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle exec rspec spec bullet-7.1.4/update.sh000077500000000000000000000010171452565225300146430ustar00rootroot00000000000000BUNDLE_GEMFILE=Gemfile.rails-5.2 bundle update BUNDLE_GEMFILE=Gemfile.rails-5.1 bundle update BUNDLE_GEMFILE=Gemfile.rails-5.0 bundle update BUNDLE_GEMFILE=Gemfile.rails-4.2 bundle update BUNDLE_GEMFILE=Gemfile.rails-4.1 bundle update BUNDLE_GEMFILE=Gemfile.rails-4.0 bundle update BUNDLE_GEMFILE=Gemfile.mongoid-8.0 bundle update BUNDLE_GEMFILE=Gemfile.mongoid-7.0 bundle update BUNDLE_GEMFILE=Gemfile.mongoid-6.0 bundle update BUNDLE_GEMFILE=Gemfile.mongoid-5.0 bundle update BUNDLE_GEMFILE=Gemfile.mongoid-4.0 bundle update