Search code examples
ruby-on-railsarel

Rails - how to combine incompatible statements with OR (Rails 5)


There is a Project model and Payment model:

class Project < ApplicationRecord
  has_many :payments
end

class Payment < ApplicationRecord
  belongs_to :project
end

It is quite easy for me to combine queries for colums with either or OR and.

Project.where(projects[:created_at].gt(6.months.ago).or(projects[:updated_at].gt(1.month.ago)))

The reason for using Arel is that the exact columns, operators and values comes from the user and Arel operators match very nicely with what the user can select.

However, if I want to combine this with data from another associated table, then I just cannot figure out what to do. ActiverRecord requires OR to be structurally compatible, so I cannot do:

Project.where(projects[:created_at].gt(6.months.ago)).or(Project.joins(:payments))

Something like this: Project.where(projects[:created_at].gt(6.months.ago).or(projects.join(payments).on(projects[:id].eq(payments[:project_id]))))

raises exception: ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: subquery must return only one column.

One way I was able to do it was something like: Project.where(projects[:created_at].gt(6.months.ago)).or(Project.where(id: Project.joins(:payments).select(:id)))

which generated SQL SELECT "projects".* FROM "projects" WHERE (("projects"."created_at" > '2019-08-19 09:11:22.064298') OR "projects"."id" IN (SELECT "projects"."id" FROM "projects" INNER JOIN "payments" ON "payments"."project_id" = "projects"."id"))

Is this the best way to do it or is there a better way? Please there might be 10+ such clauses combined, involving multiple tables, using include on all the possible tables to make all individual clauses compatible is likely to be too expensive.


Solution

  • Is there any reason why you can't keep your joins consistent across conditions? For example in Rails 5+ you can write:

    Project.where(some_field: '12').joins(:payments)
           .or(
               Project.where(payments: {id: '1'}).joins(:payments)
           )
    

    Which would generate SQL like:

    SELECT projects.* FROM projects
    INNER JOIN payments ON (payments.project_id = project.id)
    WHERE projects.some_field = 12 OR payments.id = 1
    

    Note:

    Using your structurally incompatible example in the OP, you can simply add a join to the first condition to make it compatible:

    Project.where(projects[:created_at].gt(6.months.ago)).joins(:payments).or(Project.joins(:payments))