Skip to content

Instantly share code, notes, and snippets.

@nachokb
Last active February 21, 2017 20:07
Show Gist options
  • Save nachokb/a343b3f98f4d5d0bdca3 to your computer and use it in GitHub Desktop.
Save nachokb/a343b3f98f4d5d0bdca3 to your computer and use it in GitHub Desktop.
ActiveRecord: #merge with joins and a has_many :through creates redundant INNER JOINs and postgresql does not like those
system 'rm Gemfile' if File.exist?('Gemfile')
File.write('Gemfile', <<-GEMFILE)
source 'https://rubygems.org'
gem 'activerecord',
'5.0.1'
# '4.2.0'
# '4.1.8'
# '4.0.12'
# '3.2.21'
gem 'sqlite3'
GEMFILE
system 'bundle install'
require 'byebug'
require 'bundler'
Bundler.setup(:default)
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 :companies do |t|
t.string :name
end
create_table :titles do |t|
t.string :name
t.boolean :obscene_salary
end
create_table :positions do |t|
t.belongs_to :company
t.belongs_to :title
t.string :location
end
create_table :jobs do |t|
t.belongs_to :position
t.date :started_at
t.string :full_name
end
end
class Company < ActiveRecord::Base
has_many :positions
has_many :titles,
through: :positions
end
# think of titles as being regulated, companies can't just create a title
class Title < ActiveRecord::Base
has_many :positions
has_many :companies,
through: :positions
# titles which had at least one position with at least one active job on a given date
scope :active_on, -> (date) { joins(:positions).uniq.merge(Position.active_on(date)) }
end
class Position < ActiveRecord::Base
belongs_to :title
belongs_to :company
has_many :jobs
# positions which had at least one active job on a given date
scope :active_on, -> (date) { joins(:jobs).uniq.merge(Job.active_on(date)) }
end
class Job < ActiveRecord::Base
belongs_to :position
# jobs which were active on a given date
scope :active_on, -> (date) { where('started_at <= ?', date) }
def active?(date)
started_at <= date
end
end
# class BugTest < Minitest::Test
class BugTest < MiniTest::Unit::TestCase
def setup
# One Company
@acme = Company.create name: 'Acme'
# Titles
@cto = Title.create name: 'CTO', obscene_salary: true
@clerk = Title.create name: 'Clerk', obscene_salary: true
# Positions
@kl_cto = Position.create title: @cto, company: @acme, location: "King's Landing"
@kl_clerk = Position.create title: @clerk, company: @acme, location: "King's Landing"
@ss_clerk = Position.create title: @clerk, company: @acme, location: "Sunspear"
# Jobs
@albert = Job.create position: @kl_cto, started_at: Date.civil(2015, 1, 1), full_name: 'Albert Doe'
@bruce = Job.create position: @kl_clerk, started_at: Date.civil(2015, 2, 1), full_name: 'Bruce Doe'
@carl = Job.create position: @ss_clerk, started_at: Date.civil(2015, 3, 1), full_name: 'Carl Doe'
end
def test_merge_with_joins
assert_equal 2, Title.active_on(Date.civil(2015, 3, 10)).count # this is sometimes 4 ¯\_(ツ)_/¯
assert_equal 1, Title.active_on(Date.civil(2015, 1, 10)).count
end
def test_merge_with_joins_and_has_many
assert_equal 2, @acme.titles.active_on(Date.civil(2015, 3, 10)).count
assert_equal 1, @acme.titles.active_on(Date.civil(2015, 1, 10)).count
sql = @acme.titles.active_on(Date.civil(2015, 3, 10)).to_sql
puts sql
assert_equal 1,
sql.scan(/INNER JOIN .positions/).count,
'hmt and explicit joins duplicate the INNER JOIN on positions, which Postgres does not approve'
end
end
@nachokb
Copy link
Author

nachokb commented Mar 20, 2015

Actually, a more appropriate description would be that postgresql does not like the reference to "positions" in the second join ("positions"."id") when that is defined later. Query:

SELECT DISTINCT "titles".* 
  FROM "titles"
    INNER JOIN "positions" "positions_titles"
      ON "positions_titles"."title_id" = "titles"."id"
    LEFT OUTER JOIN "jobs"
      ON "jobs"."position_id" = "positions"."id"
    INNER JOIN "positions"
      ON "titles"."id" = "positions"."title_id"
  WHERE "positions"."company_id" = 2
    AND (started_at <= '2015-03-10')

This is what Postgresql says with this query:

invalid reference to FROM-clause entry for table "positions"
HINT:  Perhaps you meant to reference the table alias "positions_titles".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment