#57631·rails

ActiveRecord .where.missing generates wrong SQL when association with explicite "class_name" matches model's table name

Author: creveteCreated Jun 9, 2026Updated Sep 10, 2026

Steps to reproduce

Models definition with namespace:

# app/models/stock_snapshots.rb
module StockSnapshots
  def self.table_name_prefix = "stock_snapshots_"
end

# app/models/stock_snapshots/zones_group.rb
module StockSnapshots
  class ZonesGroup < ApplicationRecord
    has_many :stock_snapshots_zones_groups, class_name: "StockSnapshots::StockSnapshotsZonesGroup"
    ...
  end
end

# app/models/stock_snapshots/stock_snapshots_zones_group.rb
module StockSnapshots
  class StockSnapshotsZonesGroup < ApplicationRecord
    belongs_to :zones_group, class_name: "StockSnapshots::ZonesGroup"
    ...
  end
end

In order to find StockSnapshots::ZonesGroup without associated StockSnapshots::StockSnapshotsZonesGroup:

StockSnapshots::ZonesGroup.where.missing(:stock_snapshots_zones_groups)

Expected behavior

The .where.missing query generates the following SQL:

SELECT "stock_snapshots_zones_groups".* FROM "stock_snapshots_zones_groups" LEFT OUTER JOIN "stock_snapshots_stock_snapshots_zones_groups" ON "stock_snapshots_stock_snapshots_zones_groups"."zones_group_id" = "stock_snapshots_zones_groups"."id" WHERE "stock_snapshots_stock_snapshots_zones_groups"."id" IS NULL

Actual behavior

4.0.2 :001 > puts StockSnapshots::ZonesGroup.where.missing(:stock_snapshots_zones_groups).to_sql
SELECT "stock_snapshots_zones_groups".* FROM "stock_snapshots_zones_groups" LEFT OUTER JOIN "stock_snapshots_stock_snapshots_zones_groups" "stock_snapshots_zones_groups_stock_snapshots_zones_groups" ON "stock_snapshots_zones_groups_stock_snapshots_zones_groups"."zones_group_id" = "stock_snapshots_zones_groups"."id" WHERE "stock_snapshots_zones_groups"."id" IS NULL
 => nil

The where clause is completely wrong since "stock_snapshots_zones_groups" is the main table rather than the alias name ("stock_snapshots_zones_groups_stock_snapshots_zones_groups") generated by "left_joins".

When checking the source code:

def missing(*associations)
  associations.each do |association|
    reflection = scope_association_reflection(association)
    @scope.left_outer_joins!(association)
    association_conditions = Array(reflection.association_primary_key).index_with(nil)
    if reflection.options[:class_name]
      # ==> THE BUG IS RIGHT HERE: "association" may perfectly matches the main table's name
      @scope.where!(association => association_conditions)
    else
      @scope.where!(reflection.table_name => association_conditions)
    end
  end

  @scope
end

If the options[:class_name] is specified, then AR simply take the association name rather than the alias name of the main table generated by left_outer_joins!. Unfortunately the association name in this case perfectly matches the main table name ("stock_snapshots_zones_groups")...

⚠️ Not tested, but I guess the same issue DOES ALSO EXIST in #associated method.

Workaround

4.0.2 :002 > puts StockSnapshots::ZonesGroup.left_joins(:stock_snapshots_zones_groups).where(stock_snapshots_stock_snapshots_zones_groups: { id: nil }).to_sql
SELECT "stock_snapshots_zones_groups".* FROM "stock_snapshots_zones_groups" LEFT OUTER JOIN "stock_snapshots_stock_snapshots_zones_groups" ON "stock_snapshots_stock_snapshots_zones_groups"."zones_group_id" = "stock_snapshots_zones_groups"."id" WHERE "stock_snapshots_stock_snapshots_zones_groups"."id" IS NULL
 => nil

The basic query combining "left_joins" and "where" generates the correct SQL.

System configuration

Rails version: 8.1.3

Ruby version: ruby 4.0.2