Skip to content

Instantly share code, notes, and snippets.

@pcreux
Created July 23, 2013 17:25
Show Gist options
  • Select an option

  • Save pcreux/6064275 to your computer and use it in GitHub Desktop.

Select an option

Save pcreux/6064275 to your computer and use it in GitHub Desktop.

Revisions

  1. pcreux created this gist Jul 23, 2013.
    80 changes: 80 additions & 0 deletions cancan_use_subqueries.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,80 @@
    require 'cancan/model_adapters/active_record_adapter'

    class CanCan::ModelAdapters::ActiveRecordAdapter
    # From CanCan
    def database_records
    if override_scope
    @model_class.scoped.merge(override_scope)
    elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
    mergeable_conditions = @rules.select {|rule| rule.unmergeable? }.blank?
    if mergeable_conditions
    # TODO Only use nested queries when necessary
    # Let's use nested queries
    BuildNestedRelation.call(@model_class, @rules)
    else
    @model_class.where(*(@rules.map(&:conditions))).joins(joins)
    end
    else
    @model_class.scoped(:conditions => conditions, :joins => joins)
    end
    end

    class BuildNestedRelation
    def self.call(*args)
    new(*args).call
    end

    def initialize(model_class, rules)
    self.model_class = model_class
    self.rules = rules
    end

    def call
    model_class.where(query)
    end

    private

    # @return [String] like:
    # id IN ( SUBQUERY_FOR_RULE_1 ) OR id IN ( SUBQUERY_FOR_RULE_2 ) ...
    #
    def query
    rules.map do |rule|
    "#{select_id} IN (
    #{subquery(rule).to_sql}
    )"
    end.join("\nOR\n")
    end

    def select_id
    %|"#{model_class.table_name}"."id"|
    end

    # @return [ActiveRecord::Relation] a subquery for the rule passed in
    def subquery(rule)
    model_class.
    select(select_id).
    joins(joins(rule)).
    where(conditions(rule)).
    reorder(nil). # work around ambiguous column error
    limit(nil). # don't take limit into account
    offset(nil) # don't take offset into account
    end

    # @return conditions statement for the rule passed in
    def conditions(rule)
    cancan_adapter(rule).conditions
    end

    # @return joins statement for the rule passed in
    def joins(rule)
    cancan_adapter(rule).joins
    end

    def cancan_adapter(rule)
    CanCan::ModelAdapters::ActiveRecordAdapter.new(model_class, [rule])
    end

    attr_accessor :model_class, :rules
    end
    end