From 58c7204423bffd7d4e4b1952a6ab56afcb571d31 Mon Sep 17 00:00:00 2001 From: kaspernj Date: Thu, 13 May 2021 11:06:18 +0200 Subject: [PATCH] Added the new strategy: double_exist_subquery --- lib/cancan/config.rb | 2 +- .../model_adapters/active_record_5_adapter.rb | 50 +++++++++++++++++-- .../has_and_belongs_to_many_spec.rb | 31 ++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/lib/cancan/config.rb b/lib/cancan/config.rb index a9106526b..43eac5b91 100644 --- a/lib/cancan/config.rb +++ b/lib/cancan/config.rb @@ -3,7 +3,7 @@ module CanCan def self.valid_accessible_by_strategies strategies = [:left_join] - strategies << :subquery unless does_not_support_subquery_strategy? + strategies.push(:double_exist_subquery, :subquery) unless does_not_support_subquery_strategy? strategies end diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index 072ed7f50..1dd9b412f 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -21,17 +21,57 @@ def self.matches_condition?(subject, name, value) private + delegate :connection, :quoted_primary_key, to: :@model_class + delegate :quote_table_name, to: :connection + def build_joins_relation(relation, *where_conditions) case CanCan.accessible_by_strategy when :subquery - inner = @model_class.unscoped do - @model_class.left_joins(joins).where(*where_conditions) - end - @model_class.where(@model_class.primary_key => inner) - + build_joins_relation_subquery(where_conditions) when :left_join relation.left_joins(joins).distinct + when :double_exist_subquery + build_joins_relation_double_exist_subquery(where_conditions) + end + end + + def build_joins_relation_subquery(where_conditions) + inner = @model_class.unscoped do + @model_class.left_joins(joins).where(*where_conditions) end + @model_class.where(@model_class.primary_key => inner) + end + + def build_joins_relation_double_exist_subquery(where_conditions) + @model_class.where("EXISTS (#{double_exists_query_sql(where_conditions)})") + end + + def double_exists_inner_query(where_conditions) + @model_class + .unscoped + .select('1') + .left_joins(joins) + .where(*where_conditions) + .where( + "#{quoted_table_name}.#{quoted_primary_key} = " \ + "#{quoted_aliased_table_name}.#{quoted_primary_key}" + ) + end + + def double_exists_query_sql(where_conditions) + 'SELECT 1 ' \ + "FROM #{quoted_table_name} AS #{quoted_aliased_table_name} " \ + 'WHERE ' \ + "#{quoted_aliased_table_name}.#{quoted_primary_key} = #{quoted_table_name}.#{quoted_primary_key} AND " \ + "EXISTS (#{double_exists_inner_query(where_conditions).to_sql})" + end + + def quoted_aliased_table_name + @quoted_aliased_table_name ||= quote_table_name('aliased_table') + end + + def quoted_table_name + @quoted_table_name ||= quote_table_name(@model_class.table_name) end def sanitize_sql(conditions) diff --git a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb index 0d112e9e0..67321e458 100644 --- a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb +++ b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb @@ -46,6 +46,37 @@ class House < ActiveRecord::Base end unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + describe 'fetching of records - double_exist_subquery strategy' do + before do + CanCan.accessible_by_strategy = :double_exist_subquery + end + + it 'it retreives the records correctly' do + houses = House.accessible_by(ability) + expect(houses).to match_array [@house2, @house1] + end + + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + it 'generates the correct query' do + expect(ability.model_adapter(House, :read)) + .to generate_sql("SELECT \"houses\".* + FROM \"houses\" + WHERE (EXISTS (SELECT 1 + FROM \"houses\" AS \"aliased_table\" + WHERE + \"aliased_table\".\"id\" = \"houses\".\"id\" AND + EXISTS (SELECT 1 + FROM \"houses\" + LEFT OUTER JOIN \"houses_people\" ON \"houses_people\".\"house_id\" = \"houses\".\"id\" + LEFT OUTER JOIN \"people\" ON \"people\".\"id\" = \"houses_people\".\"person_id\" + WHERE + \"people\".\"id\" = #{@person1.id} AND + (\"houses\".\"id\" = \"aliased_table\".\"id\")))) + ") + end + end + end + describe 'fetching of records - subquery strategy' do before do CanCan.accessible_by_strategy = :subquery