ActiveRecord#merge not using table alias

This is related to this github issue, I would like to confirm if it is a bug or the expected behavior.

ActiveRecord#merge not uses the table alias.
It makes the query throw an ActiveRecord::StatementInvalid when it is built using the following pattern:

  • (A) joins an aliased relation
    • has_one :singular_name
    • belongs_to :singular_name
    • has_many :custom_name, class: "TheModel"
  • (B) uses a condition referring the aliased relation
    • where(singular_name: {id: 1})
    • order(singular_name: [:id])
  • (C) merge a scope from the aliased relation
    • merge(TheModel.some_scope)

The (A) and (B) part of the query will use the alias, but the (C) will not.

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "rails", github: "rails/rails"
  # If you want to test against edge Rails replace the previous line with this:
  # gem "rails", github: "rails/rails", branch: "main"

  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :orders, force: true do |t|
    t.integer :customer_id
    t.datetime :due_date
  end

  create_table :order_items, force: true do |t|
    t.integer :order_id
  end
end

class Order < ActiveRecord::Base
  has_many :order_items

  scope :delayed, -> { where(due_date: (Time.now..)) }
end

class OrderItem < ActiveRecord::Base
  belongs_to :order
end

class BugTest < Minitest::Test
  def test_association_stuff
    # ok if where is added to the merged relation
    OrderItem.joins(:order).merge(Order.where(customer_id: 1).delayed).take

    # ok if where uses the unaliased table name and a merged scope
    OrderItem.joins(:order).where(orders: { customer_id: 1 }).merge(Order.delayed).take

    # ActiveRecord::StatementInvalid error if where uses the aliased name and a merged scope
    OrderItem.joins(:order).where(order: { customer_id: 1 }).merge(Order.delayed).take
  end
end
1 Like

I respect that this is a hope to improve Rails – and just wanted to offer a different option to make things like this work –

One way to reliably use the “alias” name (the AR association name) as a part of a .where(), .select(), .group() or .pluck() is with Brick, which parses any amount of dotted pathing of these names like this:

OrderItem.brick_where('order.customer_id' => 1).merge(Order.delayed).take

Note that there is no .join() in this solution – remarkably this gem identifies when you’re missing any JOINs to get to those associated tables, such as order.customer_id, and intelligently adds those JOINs into place. Even if it were multiple “hops” away, such as if you had order.customer.address.city_name or something then it will automatically JOIN all needed tables and get to city_name from the Address model, 3 hops away from OrderItem in this example.

1 Like