ruby-on-railsruby-on-rails-3unionactive-relation

How do I write a UNION chain with ActiveRelation?


I need to be able to chain an arbitrary number of sub-selects with UNION using ActiveRelation.

I'm a little confused by the ARel implementation of this, since it seems to assume UNION is a binary operation.

However:

( select_statement_a ) UNION ( select_statement_b ) UNION ( select_statement_c )

is valid SQL. Is this possible without doing nasty string-substitution?


Solution

  • You can do a bit better than what Adam Lassek has proposed though he is on the right track. I've just solved a similar problem trying to get a friends list from a social network model. Friends can be aquired automatically in various ways but I would like to have an ActiveRelation friendly query method that can handle further chaining. So I have

    class User
        has_many :events_as_owner, :class_name => "Event", :inverse_of => :owner, :foreign_key => :owner_id, :dependent => :destroy
        has_many :events_as_guest, :through => :invitations, :source => :event
    
          def friends
    
    
            friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}}
            friends_as_hosts  = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}}
    
            User.where do
              (id.in friends_as_guests.select{id}
              ) | 
              (id.in friends_as_hosts.select{id}
              )
            end
           end
    
    end
    

    which takes advantage of Squeels subquery support. Generated SQL is

    SELECT "users".* 
    FROM   "users" 
    WHERE  (( "users"."id" IN (SELECT "users"."id" 
                               FROM   "users" 
                                      INNER JOIN "invitations" 
                                        ON "invitations"."user_id" = "users"."id" 
                                      INNER JOIN "events" 
                                        ON "events"."id" = "invitations"."event_id" 
                               WHERE  "events"."owner_id" = 87) 
               OR "users"."id" IN (SELECT "users"."id" 
                                   FROM   "users" 
                                          INNER JOIN "events" 
                                            ON "events"."owner_id" = "users"."id" 
                                          INNER JOIN "invitations" 
                                            ON "invitations"."user_id" = 
                                               "users"."id" 
                                   WHERE  "invitations"."user_id" = 87) )) 
    

    An alternative pattern where you need a variable number of components is demonstrated with a slight modification to the above code

      def friends
    
    
        friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}}
        friends_as_hosts  = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}}
    
        components = [friends_as_guests, friends_as_hosts]
    
        User.where do
          components = components.map { |c| id.in c.select{id} }
          components.inject do |s, i|
            s | i
          end
        end
    
    
      end
    

    And here is a rough guess as to the solution for the OP's exact question

    class Shift < ActiveRecord::Base
      def self.limit_per_day(options = {})
        options[:start]   ||= Date.today
        options[:stop]    ||= Date.today.next_month
        options[:per_day] ||= 5
    
        queries = (options[:start]..options[:stop]).map do |day|
    
          where{|s| s.scheduled_start >= day}.
          where{|s| s.scheduled_start < day.tomorrow}.
          limit(options[:per_day])
    
        end
    
        where do
          queries.map { |c| id.in c.select{id} }.inject do |s, i|
            s | i
          end
        end
      end
    end