diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e70099fb..829a99fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - gemfile: ['Gemfile.rails-7.0', 'Gemfile.rails-7.1'] + gemfile: ['Gemfile.rails-7.0', 'Gemfile.rails-7.1', 'Gemfile.rails-7.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: diff --git a/Gemfile.rails-7.0 b/Gemfile.rails-7.0 index 6569e268..04ea9202 100644 --- a/Gemfile.rails-7.0 +++ b/Gemfile.rails-7.0 @@ -3,7 +3,7 @@ source "https://rubygems.org" gemspec gem 'rails', '~> 7.0.0' -gem 'sqlite3' +gem 'sqlite3', '~> 1.4' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' diff --git a/Gemfile.rails-7.1 b/Gemfile.rails-7.1 index 970b63e6..c39a3311 100644 --- a/Gemfile.rails-7.1 +++ b/Gemfile.rails-7.1 @@ -3,7 +3,7 @@ source "https://rubygems.org" gemspec gem 'rails', '~> 7.1.0' -gem 'sqlite3' +gem 'sqlite3', '~> 1.4' gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] gem 'activerecord-import' diff --git a/Gemfile.rails-7.2 b/Gemfile.rails-7.2 new file mode 100644 index 00000000..7792241e --- /dev/null +++ b/Gemfile.rails-7.2 @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gemspec + +gem "rails", ">= 7.2.0.beta2", "< 7.3" +gem 'sqlite3' +gem 'activerecord-jdbcsqlite3-adapter', platforms: [:jruby] +gem 'activerecord-import' + +gem "rspec" diff --git a/lib/bullet/active_record72.rb b/lib/bullet/active_record72.rb new file mode 100644 index 00000000..e92a4531 --- /dev/null +++ b/lib/bullet/active_record72.rb @@ -0,0 +1,318 @@ +# 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, allow_retry: false, &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 diff --git a/lib/bullet/dependency.rb b/lib/bullet/dependency.rb index 2b855267..0167dffc 100644 --- a/lib/bullet/dependency.rb +++ b/lib/bullet/dependency.rb @@ -33,6 +33,8 @@ def active_record_version 'active_record70' elsif active_record71? 'active_record71' + elsif active_record72? + 'active_record72' else raise "Bullet does not support active_record #{::ActiveRecord::VERSION::STRING} yet" end @@ -114,6 +116,10 @@ def active_record71? active_record7? && ::ActiveRecord::VERSION::MINOR == 1 end + def active_record72? + active_record7? && ::ActiveRecord::VERSION::MINOR == 2 + end + def mongoid4x? mongoid? && ::Mongoid::VERSION =~ /\A4/ end diff --git a/test.sh b/test.sh index c26272cc..fa0d44cc 100755 --- a/test.sh +++ b/test.sh @@ -1,5 +1,6 @@ #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.2 bundle && BUNDLE_GEMFILE=Gemfile.rails-7.2 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